back

React useContext with useReducer

code snippet

A sample from a DEV Community post 1.

# init
npx create-react-app@5.0.1 react-context-with-usereducer
cd react-context-with-usereducer
app-context.js
import React, { useMemo, useReducer, createContext, useContext } from 'react';
import { initialState, contextReducer } from './app-context-reducer';
import contextActions from './app-context-actions';

const AppContext = createContext();

function AppContextProvider({ children }) {
  const [state, dispatch] = useReducer(contextReducer, initialState);
  const value = useMemo(() => [state, dispatch], [state]);

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

function useAppContext() {
  const context = useContext(AppContext);
  if (context === undefined) {
    throw new Error('useAppContext must be used within a AppContextProvider');
  }
  const [state, dispatch] = context;
  const appContextAction = contextActions(dispatch);
  // const appContextSelector = contextSelectors(state);
  return { state, appContextAction };
}

export { AppContextProvider, useAppContext };

app-context-types.js
export const OPEN_NAV_MENU = 'OPEN_NAV_MENU';
export const CLOSE_NAV_MENU = 'CLOSE_NAV_MENU';
export const COLLAPSE_NAV_MENU = 'COLLAPSE_NAV_MENU';

app-context-reducer.js
import * as actionTypes from './app-context-types';

// Define the initial state for the context
export const initialState = {
  isNavMenuClose: false,
};

// Define the reducer function for the context
export function contextReducer(state, action) {
  switch (action.type) {
    // Handle the OPEN_NAV_MENU action
    case actionTypes.OPEN_NAV_MENU:
      return {
        ...state,
        isNavMenuClose: false,
      };
    // Handle the CLOSE_NAV_MENU action
    case actionTypes.CLOSE_NAV_MENU:
      return {
        ...state,
        isNavMenuClose: true,
      };
    // Handle the COLLAPSE_NAV_MENU action
    case actionTypes.COLLAPSE_NAV_MENU:
      return {
        ...state,
        isNavMenuClose: !state.isNavMenuClose,
      };
    // Throw an error for any unhandled action types
    default: {
      throw new Error(`Unhandled action type: ${action.type}`);
    }
  }
}

app-context-actions.js
import * as actionTypes from './app-context-types';

// Define a function that returns an object with context actions
const contextActions = (dispatch) => {
  return {
    navMenu: {
      // Action for opening the navigation menu
      open: () => {
        dispatch({ type: actionTypes.OPEN_NAV_MENU });
      },
      // Action for closing the navigation menu
      close: () => {
        dispatch({ type: actionTypes.CLOSE_NAV_MENU });
      },
      // Action for toggling (collapsing/expanding) the navigation menu
      collapse: () => {
        dispatch({ type: actionTypes.COLLAPSE_NAV_MENU });
      },
    },
  };
};

export default contextActions;

App.js
import logo from './logo.svg';
import './App.css';
import { AppContextProvider } from './app-context';
import Sample from './Sample';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <AppContextProvider>
          <Sample />
        </AppContextProvider>
        <br/>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;

App.css
.App {
  text-align: center;
}

.App-logo {
  height: 40vmin;
  pointer-events: none;
}

@media (prefers-reduced-motion: no-preference) {
  .App-logo {
    animation: App-logo-spin infinite 20s linear;
  }
}

.App-header {
  background-color: #282c34;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-size: calc(10px + 2vmin);
  color: white;
}

.App-link {
  color: #61dafb;
}

@keyframes App-logo-spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

index.css
body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

code {
  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
    monospace;
}

Sample.js
import { useAppContext } from './app-context';

function Sample() {
  const { state: stateApp, appContextAction } = useAppContext();
  const { isNavMenuClose } = stateApp;
  const { navMenu } = appContextAction;

  const onCollapse = () => navMenu.collapse();
  const onOpen = () => navMenu.open();
  const onClose = () => navMenu.close();

  return (
    <>
      <span>{'my state:'}</span>
      <span>{`${JSON.stringify(stateApp)}`}</span>
      <button type="button" onClick={onOpen}>Open</button>
      <button type="button" onClick={onClose}>Close</button>
      <button type="button" onClick={onCollapse}>Collapse</button>
    </>
  );
}

export default Sample;

After creating all files, the repo should look like below.

 |-src
 | |-app-context.js
 | |-app-context-types.js
 | |-app-context-reducer.js
 | |-app-context-actions.js
 | |-App.js
 | |-App.css
 | |-index.js
 | |-index.css
 | |-Sample.js
# begin by typing:
cd react-context-with-usereducer
npm start

# starts the development server.
npm start

# bundles the app into static files for production.
npm run build

# starts the test runner.
npm test

# removes this tool and copies build dependencies, configuration files
# and scripts into the app directory. If you do this, you can’t go back!
npm run eject
# GET request
curl localhost:3000