SPG
SPG

Reputation: 6197

How to dispatch action in Custom Hooks by useReducer and useContext?

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!!!

Upvotes: 4

Views: 7551

Answers (2)

Dennis Vash
Dennis Vash

Reputation: 53874

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.

Upvotes: 2

HMR
HMR

Reputation: 39260

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>

Upvotes: 3

Related Questions