1

This is my first time using the React context hooks in an app and I am trying to set the SelectedBackgroundContext and it will not update. console.log(typeof selectBackground) does appear as a function so I believe it is importing correctly and am not sure why it's not updating.

All of my code can be found in this CodeSandbox link below. The line I am running into issues with is child.js:8.

child

export default function Child() {
  const { selectedBackground } = useContext(SelectedBackgroundContext);
  const { selectBackground } = useContext(SelectedBackgroundContext);

  selectBackground(null); //Should render text saying "None" instead of image
  console.log(selectedBackground);

  const renderSelected = (context) => {
    if (context) {
      return (
        <img
          style={{ height: "200px" }}
          src={context}
          key={context + "Thumbnail"}
          alt={"thumbnail of " + context}
        />
      );
    } else {
      return <p>None</p>;
    }
  };

  return (
    <div>
      <p>Background:</p> {renderSelected(selectedBackground)}
    </div>
  );
}

context

export const SelectedBackgroundContext = React.createContext({
  selectedBackground:
    "https://lp-cms-production.imgix.net/2019-06/81377873%20.jpg?fit=crop&q=40&sharp=10&vib=20&auto=format&ixlib=react-8.6.4",
  selectBackground: () => {}
});

Edit React Context Troubleshooting

I would be grateful for any advice!

Sanket Shah
  • 2,888
  • 1
  • 11
  • 22
ariel-walley
  • 17
  • 3
  • 9

2 Answers2

1

Issue

You are trying to provide the context value as the context value, which of course won't work at all. You are effectively providing the default context value to your app because App has no SelectedBackgroundContext context provider above it in the ReactTree.

You've also coded an unintentional side-effect in Child when you update the context value directly from the function budy.

selectBackground(null); // <-- unintentional side-effect
console.log(selectedBackground); // <-- doesn't log updated state immediately

Solution

App need to "fill" in the selectedBackground and selectBackground callback values. App itself can't use the SelectedBackgroundContext it is providing. App should have some local component state to store, and update, the selectedBackground value.

function App() {
  const [selectedBackground, selectBackground] = useState(
    "https://lp-cms-production.imgix.net/2019-06/81377873%20.jpg?fit=crop&q=40&sharp=10&vib=20&auto=format&ixlib=react-8.6.4"
  );

  return (
    <SelectedBackgroundContext.Provider
      value={{ selectedBackground, selectBackground }}
    >
      <Child />
    </SelectedBackgroundContext.Provider>
  );
}

In Child use useEffect hooks to issue side-effects and "listen" for changes to values.

import React, { useEffect, useContext } from "react";
import { SelectedBackgroundContext } from "./context";

export default function Child() {
  const { selectBackground, selectedBackground } = useContext(
    SelectedBackgroundContext
  );

  useEffect(() => {
    selectBackground(null);
  }, [selectBackground])

  useEffect(() => {
    console.log(selectedBackground);
  }, [selectedBackground])

  const renderSelected = (context) => {
    if (context) {
      return (
        <img
          style={{ height: "200px" }}
          src={context}
          key={context + "Thumbnail"}
          alt={"thumbnail of " + context}
        />
      );
    } else {
      return <p>None</p>;
    }
  };

  return (
    <div>
      <p>Background:</p> {renderSelected(selectedBackground)}
    </div>
  );
}

Edit react-usecontext-not-updating

Drew Reese
  • 165,259
  • 14
  • 153
  • 181
  • Thank you for this!, I'm disappointed to see this is how context works, I would like the component to re-render if the context changes automatically. I think for others wanting this behavior as well they should checkout mobx.js – ZackOfAllTrades Feb 25 '23 at 19:12
  • @ZackOfAllTrades I don't follow your comment, the component does render when the provider updates. In fact, the entire sub-ReactTree below the provider rerenders. The rerender is what allows the `useEffect` hook to pick up a dependency change. – Drew Reese Feb 25 '23 at 21:57
0

The link below shows my preferred boilerplate for handling context (you can press the button to toggle the background on or off).

codesandbox link

The boilerplate has the following structure, which follows the philosophy of redux.

├── package.json
└── src
    ├── App.js
    ├── child.js
    ├── context
    │   ├── actions.js
    │   ├── reducer.js
    │   └── store.js
    └── index.js

action.js

export const actions = {
  SET_BACKGROUND: "SET_BACKGROUND"
};

It lists all the allowed action in the context.

reducer.js

import { actions } from "./actions.js";

export const reducer = (state, action) => {
  switch (action.type) {
    case actions.SET_BACKGROUND:
      return { ...state, background: action.background };
    default:
      return state;
  }
};

This is where actual change is made to the context. Reducer first check action type,. Then based on the action type, it makes corresponding modification to the context state.

store.js

import * as React from "react";
import { reducer } from "./reducer.js";
import { actions } from "./actions.js";

import { originalBackground } from "../child";

export const initialState = {
  background: originalBackground
};

export const AppContext = React.createContext();

export const Provider = (props) => {
  const { children } = props;
  const [state, dispatch] = React.useReducer(reducer, initialState);

  const value = {
    background: state.background,
    setBackground: React.useCallback(
      (val) => dispatch({ type: actions.SET_BACKGROUND, background: val }),
      []
    )
  };

  return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
};

This is where the context state is stored. The context state contains values (e.g. background) and functions to trigger the modification of such values (e.g. setBackground). Note that the function itself does not modify anything. It dispatches an action, which will be caught by the reducer for the actual state modification.

App.js

import React from "react";
import { Provider } from "./context/store";
import Child from "./child";

function App() {
  return (
    <Provider>
      <Child />
    </Provider>
  );
}

export default App;

Wrap the Provider over the component where the context is accessed. If the context is global to the entire app, Provider can wrap around App in index.js.

child.js

import React, { useContext } from "react";
import { AppContext } from "./context/store";

export const originalBackground =
  "https://lp-cms-production.imgix.net/2019-06/81377873%20.jpg?fit=crop&q=40&sharp=10&vib=20&auto=format&ixlib=react-8.6.4";

export default function Child() {
  const { background, setBackground } = useContext(AppContext);

  const renderSelected = (context) => {
    if (context) {
      return (
        <img
          style={{ height: "200px" }}
          src={context}
          key={context + "Thumbnail"}
          alt={"thumbnail of " + context}
        />
      );
    } else {
      return <p>None</p>;
    }
  };

  const toggleBackground = () => {
    if (background) {
      setBackground(null);
    } else {
      setBackground(originalBackground);
    }
  };

  return (
    <div>
      <button onClick={toggleBackground}>
        {background ? "Background OFF" : "Background ON"}
      </button>
      <p>Background:</p> {renderSelected(background)}
    </div>
  );
}

This shows how context is used in a component.

Fanchen Bao
  • 3,310
  • 1
  • 21
  • 34