Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React useContext not passing value down to deeply nested children

I'm brand new to the useContext hook and trying to figure out what I'm doing wrong... without the top imports, here's what my code looks like:

So the root of my project looks like this:

export default function App() {

  return (
    <div className="App">
      <AppProvider>
        <Header/>
        <Toolbar/>
        <Canvas/>
        <Imports/>
        <Footer/>
      </AppProvider>
    </div>
  );
}

Then I have a separate file called AppProvider that has this:

const CanvasItems = createContext();
const UpdateCanvasItems = createContext();

export function useCanvasItems() {
  return useContext(CanvasItems)
}
export function useChangeItems() {
  return useContext(UpdateCanvasItems)
}

export default function AppProvider({children}) {
  const [state, setState] = useState({
    imports: [],
    images: [],
    text: [],
    circles: [],
    rectangles: [],
    draw: [],
  })

  // function pushes new items in to state object
  function addCanvasItem(type, source) {
    if (type === 'import') {
      const tempState = [...state.imports];
      tempState.push({id: Date.now(), image: source});
      setState({...state, imports: tempState})

    } else if (type === 'image') {
      const tempState =  [...state.images];
      tempState.push({
        id: Date.now(),
        src: source.src,
        x: window.innerWidth * Math.random(),
        y: window.innerHeight * Math.random(),
      })
      setState({...state, images: tempState})
    }
    console.log("state after new item added:", state)
  }

  return (
    <CanvasItems.Provider value={state}>
        <UpdateCanvasItems.Provider value={addCanvasItem}>
          {children}
        </UpdateCanvasItems.Provider>
    </CanvasItems.Provider>
  )
}

I don't have any problems passing the state object and the addCanvasItems function down to the first level of children (so Header, Toolbar, Canvas, etc.) but I have children inside those components where I can't access the values.

For clarity - inside the Canvas component, I have a Stage component, inside that there's a Layer component, inside that there's an Items component (I'm using Konva and need to have all this nesting).

export default function Canvas() {
  const { onDrop, onDragOver } = useDragDrop;
  const state = useCanvasItems();

  useEffect(() => {
    console.log("state in canvas", state)
  }, [state])
  
  return (
      <div id='canvas'>
        <Stage
          container='canvas'
          onDrop={onDrop}
          onDragOver={onDragOver}
        >
          <Layer>
            <Items/>
          </Layer>
        </Stage>
      </div>
  )
}

I need to access the state object in the Items component but I want to avoid prop drilling. For testing, I created a useEffect function to print the state every time it changes. The Items component prints undefined on the first load, whereas the Canvas component prints the full object.

export default function Items() {
  const state = useCanvasItems();

  useEffect(() => {
    console.log("state in items", state)
  }, [state])

  return (
    <>
      {state && state.images.map(image => {
          return <NewImage image={image} />
      })}
    </>
  )
};

what prints to the console on load and after a file is imported (changing the state)

What am I missing with this hook and how to implement it??

MANY thanks for the help!! 🙏🙏🙏🙏

like image 458
Meghan Hein Avatar asked Oct 21 '20 02:10

Meghan Hein


1 Answers

If you look at the return value of Stage it does not render children, meaning Layer and Items are being treated as special cases. I think you're best bet is to pass props to them instead of relying on context since they appear to technically not be part of React's rendering context at all, but part of Konva's custom renderer: KonvaRenderer

https://github.com/konvajs/react-konva/blob/master/src/ReactKonvaCore.js#L89

  // Stage component
  render() {
    const props = this.props;

    return (
      <div
        ref={(ref) => (this._tagRef = ref)}
        accessKey={props.accessKey}
        className={props.className}
        role={props.role}
        style={props.style}
        tabIndex={props.tabIndex}
        title={props.title}
      />
    );
  }

There's an explanation in the docs about how to do this properly:

https://github.com/konvajs/react-konva#usage-with-react-context

Due to a known issue with React, Contexts are not accessible by children of the react-konva Stage component. If you need to subscribe to a context from within the Stage, you need to "bridge" the context by creating a Provider as a child of the Stage. For more info, see this discussion and this react-redux demo. Here is an example of bridging the context (live demo):

Try adding your providers again as children of Stage:

<Stage
  container='canvas'
  onDrop={onDrop}
  onDragOver={onDragOver}
>
  <CanvasItems.Provider value={state}>
    <UpdateCanvasItems.Provider value={addCanvasItem}>
      <Layer>
        <Items />
      </Layer>    
    </UpdateCanvasItems.Provider>
  </CanvasItems.Provider>
</Stage>
like image 152
azium Avatar answered Oct 29 '22 05:10

azium