Skip to content

Instantly share code, notes, and snippets.

@ginpei
Last active May 6, 2023 00:01
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ginpei/5076d4332ef53453456b8ce2b0828752 to your computer and use it in GitHub Desktop.
Save ginpei/5076d4332ef53453456b8ce2b0828752 to your computer and use it in GitHub Desktop.
Use React context with reducer

Use React context with reducer

Summary

Give state and dispatch produced by useReducer() to context created by createContext() so that you can access the values smoothly in any components placed under <Context.Provider>.

Types

XxxState

The value which your context holds.

type CounterState = { count: number }

XxxAction

The reducer actions which directs how to modifies XxxState value.

type CounterAction =
  | { action: 'increment'; data: { amount: number } }
  | { action: 'reset' }

Instances

Initial context value

Optional but useful.

const initialCounterState: CounterState = { count: 0 };

Reducer function

Function modifies XxxState value following XxxAction direction.

function reduceCounter(state: CounterState, action: CounterAction): CounterState {
  if (action.type === 'increment') {
    return { count: state.count + action.data.amount };
  }

  if (action.type === 'reset') {
    return { count: 0 };
  }

  return state;
}

Context instance

Use React.createContext().

const CounterContext = createContext({
  dispatch: (() => undefined) as React.Dispatch<CounterAction>,
  state: { ...initialCounterState },
});

These dispatch and state are never used as long as you use the context under provider. If you thought this should be optional, see

DefinitelyTyped/DefinitelyTyped#24509 (comment)

Note: don't use another one; vm.createContext() is wrong.

Use in components

Root component

The top level component provides context with values.

  1. Wrap all with the context provider: <XxxContext.Provider>
  2. Prepare a pair of state and dispatcher by React.useReducer()
  3. Give them to the provider
  4. Add any child components under the provider
const CounterPage: React.FC = () => {
  const [state, dispatch] = useReducer(reduceCounter, { ...initialCounterState });

  return (
    <CounterContext.Provider value={{ state, dispatch }}>
      <div className="CounterPage">
        <h1>Counter</h1>
        {/* add child components here */}
      </div>
    </CounterContext.Provider>
  );
};

Child components

  1. Retrieve context by React.useContext()
  2. Use values and/or invoke dispatcher

To use context state value:

const ChildCounter: React.FC = () => {
  const { state: { count } } = useContext(CounterContext);
  return (
    <div className="ChildCounter">
      Count: {count}
    </div>
  );
};

To invoke reducer function:

const CounterForm: React.FC = () => {
  const { dispatch } = useContext(CounterContext);

  const onIncrementClick = useCallback(() => {
    dispatch({ type: 'increment', data: { amount: 1 } });
  }, []);

  const onResetClick = useCallback(() => {
    dispatch({ type: 'reset' });
  }, []);

  return (
    <div className="CounterForm">
      <button onClick={onIncrementClick}>+1</button>
      <button onClick={onResetClick}>Reset</button>
    </div>
  );
};

Make sure these components are placed under <XxxContext.Provider> otherwise the context values would be the initial ones always.

import React, {
createContext, useReducer, useContext, useCallback,
} from 'react';
type CounterState = { count: number }
type CounterAction =
| { type: 'increment'; data: { amount: number } }
| { type: 'reset' }
function reduceCounter(state: CounterState, action: CounterAction): CounterState {
if (action.type === 'increment') {
return { count: state.count + action.data.amount };
}
if (action.type === 'reset') {
return { count: 0 };
}
return state;
}
const initialCounterState: CounterState = { count: 10 };
const CounterContext = createContext({
dispatch: (() => undefined) as React.Dispatch<CounterAction>,
state: { ...initialCounterState },
});
const CounterPage: React.FC = () => {
const [state, dispatch] = useReducer(reduceCounter, { ...initialCounterState });
return (
<CounterContext.Provider value={{ state, dispatch }}>
<div className="CounterPage">
<h1>Counter</h1>
<ChildCounter />
<CounterForm />
</div>
</CounterContext.Provider>
);
};
const ChildCounter: React.FC = () => {
const { state: { count } } = useContext(CounterContext);
return (
<div className="ChildCounter">
Count: {count}
</div>
);
};
const CounterForm: React.FC = () => {
const { dispatch } = useContext(CounterContext);
const onIncrementClick = useCallback(() => {
dispatch({ type: 'increment', data: { amount: 1 } });
}, []);
const onResetClick = useCallback(() => {
dispatch({ type: 'reset' });
}, []);
return (
<div className="CounterForm">
<button onClick={onIncrementClick}>+1</button>
<button onClick={onResetClick}>Reset</button>
</div>
);
};
export default CounterPage;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment