0
votes

I have an array that via useState hook, I try to add elements to the end of this array via a function that I make available through a context. However the array never gets beyond length 2, with elements [0, n] where n is the last element that I pushed after the first.

I have simplified this a bit, and haven't tested this simple of code, it isn't much more complicated though.

MyContext.tsx

interface IElement = {
  title: string;
  component: FunctionComponent<any>;
  props: any;
}

interface IMyContext = {
  history: IElement[];
  add: <T>(title: string, component: FunctionComponent<T>, props: T) => void
}

export const MyContext = React.createContext<IMyContext>({} as IMyContext)

export const MyContextProvider = (props) => {
  const [elements, setElements] = useState<IElement[]>([]);

  const add = <T extends any>(title: string, component: FunctionComponent<T>, props: T) => {
    setElements(elements.concat({title, component, props}));
  }

  return (
    <MyContext.Provider values={{elements, add}}>
      {props.children}
    </MyContext.Provider>
  );
}

In other elements I use this context to add elements and show the current list of elements, but I only ever get 2, no matter how many I add.

I add via onClick from various elements and I display via a sidebar that uses the components added.

SomeElement.tsx

const SomeElement = () => {
  const { add } = useContext(MyContext);

  return (
    <Button onClick=(() => {add('test', MyDisplay, {id: 42})})>Add</Button>
  );
};

DisplayAll.tsx

const DisplayAll = () => {
  const { elements } = useContext(MyContext);

  return (
    <>
      {elements.map((element) => React.createElement(element.component, element.props))}
    </>
  );
};
2
Where are you only seeing the two elements? Is it through history? What is history set to?Jacob
Array.concat concatinates two arrays, you are concatinating an object. It should be elements.concat([{title, component, props}])Ibraheem
Untrue, @Ibraheem. Items to concatenate can be passed in directly; you don't have to pass in an Array. Observe [].concat('woo') -> ['woo']Jacob
@Jacob thanks for that!Ibraheem
Is history supposed to be the same as elements or is there some layer in between? I'm confused because you're passing <MyContext.Provider values={{history, add}}> (also it should be just value). Just want to make sure you're instantiating your provider correctly.Jacob

2 Answers

1
votes

It sounds like your issue was in calling add multiple times in one render. You can avoid adding a ref as in your currently accepted answer by using the callback version of setState:

const add = <T extends any>(title: string, component: FunctionComponent<T>, props: T) => {
  setElements(elements => elements.concat({title, component, props}));
};

With the callback version, you'll always be sure to have a reference to the current state instead of a value in your closure that may be stale.

0
votes

The below may help if you're calling add multiple times within the same render:

interface IElement = {
  title: string;
  component: FunctionComponent<any>;
  props: any;
}

interface IMyContext = {
  history: IElement[];
  add: <T>(title: string, component: FunctionComponent<T>, props: T) => void
}

export const MyContext = React.createContext<IMyContext>({} as IMyContext)

export const MyContextProvider = (props) => {
  const [elements, setElements] = useState<IElement[]>([]);
  const currentElements = useRef(elements);

  const add = <T extends any>(title: string, component: FunctionComponent<T>, props: T) => {
    currentElements.current = [...currentElements.current, {title, component, props}];
    setElements(currentElements.current);
  }

  return (
    <MyContext.Provider values={{history, add}}>
      {props.children}
    </MyContext.Provider>
  );
}