4

I created a sample for a button toggle.

This is done by useContext (store the data) and useReducer (process the data). and it is working fine.

Here's the CodeSandBox Link to how it works.

version 1 is just dispatch when clicking the button.

Then I created a version 2 of toggling. basically just put the dispatch inside a custom hook. but somehow, it doesn't work.

// context
export const initialState = { status: false }

export const AppContext = createContext({
  state: initialState,
  dispatch: React.dispatch
})

// reducer
const reducer = (state, action) => {
  switch (action.type) {
    case 'TOGGLE':
      return {
        ...state,
        status: action.payload
      }
    default:
      return state
  }
}

//custom hook
const useDispatch = () => {
  const {state, dispatch} = useContext(AppContext)
  return {
    toggle: dispatch({type: 'UPDATE', payload: !state.status})
    // I tried to do toggle: () => dispatch(...) as well
  }
}

// component to display and interact
const Panel = () => {
  const {state, dispatch} = useContext(AppContext) 
  // use custom hook
  const { toggle } = useDispatch()
  const handleChange1 = () => dispatch({type: 'TOGGLE', payload: !state.status})
  const handleChange2 = toggle // ERROR!!!
  // and I tried handleChange2 = () => toggle, or, handleChange2 = () => toggle(), or handleChange2 = toggle()
  return (
    <div>
      <p>{ state.status ? 'On' : 'Off' }</p>
      <button onClick={handleChange1}>change version 1</button>
      <button onClick={handleChange2}>change version 2</button>
    </div>
  )
}

// root 
export default function App() {
  const [state, dispatch] = useReducer(reducer, initialState)
  return (
    <AppContext.Provider value={{state, dispatch}}>
      <div className="App">
        <Panel />
      </div>
    </AppContext.Provider>
  );
}

Not sure what's going there. but I think there's something wrong with the dispatched state.

(I tried it works if the payload is not processing state, like some hard code stuff, so the dispatch should be fired at this moment)

Could someone give me a hand? Appreciate!!!

SPG
  • 6,109
  • 14
  • 48
  • 79

2 Answers2

3

You are correct that toggle needs to be a function but you are dispatching action type UPDATE and the reducer doesn't do anything with that action.

Dennis is correct that there is no point in the initial value you are giving the context and may as well leave it empty as the provider will provide the value.

The useMemo suggestion from Dennis will not optimize your example since App re renders when state changes so the memoized value will never be used.

Here is a working example of your code with comments what I changed:

const { createContext, useReducer, useContext } = React;

const initialState = { status: false };
//no point in setting initial context value
const AppContext = createContext();

const reducer = (state, action) => {
  switch (action.type) {
    case 'TOGGLE':
      return {
        ...state,
        status: action.payload,
      };
    default:
      return state;
  }
};

const useDispatch = () => {
  const { state, dispatch } = useContext(AppContext);
  return {
    //you were correct here, toggle
    //  has to be a function
    toggle: () =>
      dispatch({
        //you dispatch UPDATE but reducer
        //  is not doing anything with that
        type: 'TOGGLE',
        payload: !state.status,
      }),
  };
};

const Panel = () => {
  const { state, dispatch } = useContext(AppContext);
  const { toggle } = useDispatch();
  const handleChange1 = () =>
    dispatch({ type: 'TOGGLE', payload: !state.status });
  const handleChange2 = toggle; // ERROR!!!
  return (
    <div>
      <p>{state.status ? 'On' : 'Off'}</p>
      <button onClick={handleChange1}>
        change version 1
      </button>
      <button onClick={handleChange2}>
        change version 2
      </button>
    </div>
  );
};

function App() {
  const [state, dispatch] = useReducer(
    reducer,
    initialState
  );
  return (
    <AppContext.Provider value={{ state, dispatch }}>
      <div className="App">
        <Panel />
      </div>
    </AppContext.Provider>
  );
}
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>


<div id="root"></div>
HMR
  • 37,593
  • 24
  • 91
  • 160
2

Well, there no such thing React.dispatch. Its value is undefined

export const AppContext = createContext({
  state: initialState,
  // useless
  // dispatch: undefined
  dispatch: React.dispatch
});

// dispatch function won't trigger anything.
const {state, dispatch} = useContext(AppContext);

version 1 is actually how context should be used, although usually, you will want to add an extra memoization step (depending on the use case), because on every render you assign a new object {state,dispatch} which always will cause a render even though state may be the same.

See such memoization use case example.

Edit black-smoke-6lz6k

If my point wasn't clear, see HMR comment:

Strategic useMemo should be used, if many components access the context then memoizing is a good idea when the component with the provider re-renders for reasons other than changing the context.

Dennis Vash
  • 50,196
  • 9
  • 100
  • 118
  • useMemo is pointless, if App is root component then it only re renders when context value changes so memoized value will never be used. If App has other reasons to re render then it would optimize things but as App is usually root component it probably won't re render for reasons other than context value changing. – HMR Jun 28 '20 at 09:31
  • It's not pointless, and as I mentioned **"depending on the use case"**, here is an example for my point: https://codesandbox.io/s/black-smoke-6lz6k?file=/index.js – Dennis Vash Jun 28 '20 at 09:38
  • In your example the context can never really changes and you may as well do `` instead of useMemo. I agree there are situations where useMemo is useful but as I stated before; the OP's component only re renders when context changes making the useMemo never memoize anything. – HMR Jun 28 '20 at 10:41
  • 1
    It wasn't a comment to solve OP question, its a recommendation, **"although usually", "depending on the use case"** – Dennis Vash Jun 28 '20 at 10:43
  • 1
    Yes, you are correct that strategic useMemo should be used if many components access the context and the component with the provider re renders for reasons other than changing the context. – HMR Jun 28 '20 at 10:45