0

I'm new to Typescript and not very familiar with the useContext hook. Basically, I have two simple components. I would like to add the items from my left component to the list on the right when I click on the button under them. My items have a name and description property. I just watch to display item.name on the side div on the right.

I would like to try and do it with useContext but I'm not sure where to start even after reading the documentation and a bunch of examples. They all seem too complicated for my tiny little example.

From what I understand, I need to:

  • Create something like AppContext.tsx
  • Create a context with createContext() // not sure about the arguments I have to put it in here with Typescript
  • Create a provider? // not sure about that either
  • Wrap my two components with the context provider

So any hint on the procedure would be appreciated. Thank you!

function App() {
  return (
    <div className="App">
      <ItemList />
      <ItemContainer />
    </div>
  );
}

My item list component:

function ItemList() {
  return (
    <div className="itemlist">
      {items.map((item, index) => (
        <div key={index}>
          <div>{item.name}</div>
          <div>{item.description}</div>
          <button>Add to sidebar</button>
        </div>
      ))}
    </div>
  );
}

And finally, my container on the right side:

function ItemContainer() {
  return (
    <div>
      <h1>List of items</h1>
      
      <p>Number of items: {}</p>
      
    </div>
  );
}
NiHaoLiHai
  • 23
  • 1
  • 8

1 Answers1

1

You can do something like this:
First create a context file named for example ItemList.context.tsx :

import React, {
  createContext,
  Dispatch,
  FunctionComponent,
  useState
} from "react";
import { Item } from "./data";
type ItemListContextType = {
  itemList: Item[]; // type of your items that I declare in data.ts
  setItemList: Dispatch<React.SetStateAction<Item[]>>; //React setState type
};
export const ItemListContext = createContext<ItemListContextType>(
  {} as ItemListContextType
);

export const ItemListContextProvider: FunctionComponent = ({ children }) => {
  const [itemList, setItemList] = useState<Item[]>([]);

  return (
    <ItemListContext.Provider
      value={{ itemList: itemList, setItemList: setItemList }}
    >
      {children}
    </ItemListContext.Provider>
  );
};


You can then add the context provider in the parent component ( App.tsx in your example):

import "./styles.css";
import ItemList from "./ItemList";
import ItemContainer from "./ItemContainer";
import { ItemListContextProvider } from "./ItemList.context";
export default function App() {
  return (
    <div className="App">
      <ItemListContextProvider>
        <ItemList />
        <ItemContainer />
      </ItemListContextProvider>
    </div>
  );
}

and you can finally access your Item List by using the hook useContext in your two components:
for ItemList.tsx where you need to set the list (and optionally get the list to avoid putting twice an item):

import { useContext } from "react";
import { data, Item } from "./data";
import { ItemListContext } from "./ItemList.context";

const items: Item[] = data;
export default function ItemList() {
  const { itemList, setItemList } = useContext(ItemListContext); // here you get your list and the method to set the list

  const addItemToItemList = (item: Item) => {
    //you are using the itemList to see if item is already in the itemList
    if (!itemList.includes(item)) setItemList((prev) => [...prev, item]);
  };
  return (
    <div className="itemlist">
      {items.map((item, index) => (
        <div style={{ marginBottom: 15 }} key={index}>
          <div style={{ fontWeight: 800 }}>{item.name}</div>
          <div>{item.description}</div>
          <button onClick={() => addItemToItemList(item)}>
            Add to sidebar
          </button>
        </div>
      ))}
    </div>
  );
}

And in your ItemContainer.tsx you only need the list so you can import only the setItemList from the context with useContext:

import { useContext } from "react";
import { ItemListContext } from "./ItemList.context";

export default function ItemContainer() {
  const { itemList } = useContext(ItemListContext);
  return (
    <div style={{ flexGrow: 4 }}>
      <h1 style={{ textAlign: "center" }}>List of items</h1>

      <p>Number of items: {itemList.length}</p>
      {itemList.length > 0 && (
        <ul>
          {itemList.map((item, i) => (
            <li key={i}>{item.name}</li>
          ))}
        </ul>
      )}
    </div>
  );
}

UPDATE with a Router
It's quite the same thing you only need to wrap your browser router in the context provider if you want it to be at the highest place in your app (for a them provider or a dark mode provider for example):

export default function App() {
  return (
    <div className="App">
    <ItemListContextProvider> 
        <BrowserRouter>
          <Switch>
            <Route exact path="/" component={HomePage} />
            <Route exact path="/itemList" component={ItemListPage} />
          </Switch>
        </BrowserRouter>
       </ItemListContextProvider> 
    </div>
  );
}

but I suggest you to put your provider the nearest place where your subscribers components are.
You can for example create a page component that will be use in the browser router and put in it the provider, like this:
ItemListPage.tsx

import React, { FunctionComponent } from "react";
import ItemList from "./ItemList";
import ItemContainer from "./ItemContainer";
import { Link } from "react-router-dom";
import { ItemListContextProvider } from "./ItemList.context";
const ItemListPage: FunctionComponent = () => {
  return (
    <>
     <ItemListContextProvider>
      <h1 style={{ alignSelf: "flex-start" }}>ITEM LIST</h1>
      <Link to="/">homePage</Link>
      <div className="itemListPage">
        <ItemList />
        <ItemContainer />
      </div>
      </ItemListContextProvider>

    </>
  );
};
export default ItemListPage;

and of course you remove the context provider in your App.tsx and it should look like :
App.tsx

import React, { FunctionComponent } from "react";
import ItemList from "./ItemList";
import ItemContainer from "./ItemContainer";
import { Link } from "react-router-dom";
import { ItemListContextProvider } from "./ItemList.context";
const ItemListPage: FunctionComponent = () => {
  return (
    <>
     <ItemListContextProvider>
      <h1 style={{ alignSelf: "flex-start" }}>ITEM LIST</h1>
      <Link to="/">homePage</Link>
      <div className="itemListPage">
        <ItemList />
        <ItemContainer />
      </div>
      </ItemListContextProvider>

    </>
  );
};
export default ItemListPage;

Edit hungry-napier-t4rcq

antoineso
  • 2,063
  • 1
  • 5
  • 13
  • Thanks for your reply! I'm still trying to understand how all this works and I'm thus trying to implement it in something more complex with React Router this time, for the challenge. So I'll mark your answer as the solution. Thanks again. I'm French too by the way :) – NiHaoLiHai Apr 08 '21 at 21:22
  • You're welcome! If you are familiar with angular is a bit like a service it's a solution to "share" props between components . But I read somewhere that you could have trouble with context if you are using Suspense but can't find the source sorry. – antoineso Apr 09 '21 at 03:25
  • I read a bunch of things about it too. I am stuck again now with Router and useContext. I created a Main.tsx with the list and the container you gave me, set up routes in App.tsx, added the context provider in Main.tsx, but when I add the context provider the whole DOM disappears and nothing loads. Nothing in the source either. I wanted to display the content from the container on another page as well. Strange issues. My router works alone. Your useContext code works alone. And when combined, everything breaks. – NiHaoLiHai Apr 09 '21 at 11:11
  • ok I will update the sandbox in the answer and tell you whenit's done – antoineso Apr 09 '21 at 11:51
  • Awesome, merci! what I did is add a button to the list container and when you click on the button, you arrive on a page which only contains the list of added items. What I coded works on its own (without the added items of course), it's when I try to use useContext with that that it breaks. Can't wait to see your implementation then. I haven't seen anything like that anywhere on the net yet for some reason. – NiHaoLiHai Apr 09 '21 at 12:14
  • oh ok so my edit answer won't help you let me try something... – antoineso Apr 09 '21 at 12:32
  • look at this sandbox with the button spec [sandBox](https://codesandbox.io/s/relaxed-morning-nter8) – antoineso Apr 09 '21 at 12:56
  • Thanks. I want to understand that because useContext allows to share data over all components, so I want to select items, and then share them in another page by clicking on a button which links to another page. I'm surprised by the degree of complexity of what seems like a trivial task... – NiHaoLiHai Apr 09 '21 at 13:00
  • Wow incredible!! Thanks!!! I'll study all this in detail now. Thank you so much! – NiHaoLiHai Apr 09 '21 at 13:02
  • Hi again, still trying to understand useContext better, say I want to add the number of times the item has been clicked next to it in my list, do I need to create a new context for it too? – NiHaoLiHai Apr 09 '21 at 16:17
  • No you can add this property in the same context. – antoineso Apr 10 '21 at 05:59