toffee.beanns
toffee.beanns

Reputation: 435

React useContext and useMemo for Global variables

Would like to seek guidance from folks if this React implementation makes sense and understand the pitfalls if any. The implementation works but I am unsure if its the correct practice. Please kindly advise.

Idea - Create an AppContext that allows reusability of global states (or even functions) - instead of the conventional useContext + useReducer

AppContext.jsx

import React from 'react';

export const AppContext = React.createContext(null);

export const AppContextProvider = (props) => {

  const [ appState, setAppState ] = React.useState({});
  const  appStateProvider = React.useMemo(() => ({ appState, setAppState }), [ appState, setAppState ]);

  const setAppStateItem = (key, value) => {
    appStateProvider.setAppState(state => { return { ...state, [key]: value} })
    return value;
  }

  const getAppStateItem = (key = '', fallback) => {
    return appState[key] || fallback;
  }

  const deleteAppStateItem = (key = '') => {
    if(key in appState) {
      appStateProvider.setAppState(state => {
         state[key] = undefined;
         return state;
      })
    }
  }

  return (
    <AppContext.Provider value={{ appStateProvider, setAppStateItem, getAppStateItem, deleteAppStateItem }}>
      {props.children}
    </AppContext.Provider>
  )
}

Create.jsx

import React from 'react';
import { AppContext } from 'contexts';

const { setAppStateItem } = React.useContext(AppContext);

....
setAppStateItem('count', 5);
....

Consume.jsx

import React from 'react';
import { AppContext } from 'contexts';

const { getAppStateItem, setAppStateItem } = React.useContext(AppContext);

....
const count = getAppStateItem('count');
....

Upvotes: 2

Views: 4708

Answers (1)

Caleb Everett
Caleb Everett

Reputation: 435

Here was an approach to create a global state using useContext and useReducer following a pattern similar to redux. You essentially set up a Store with useReducer and a Context.Provider that you then wrap the rest of your application in. Here was a small implementation I had going:

import React, { createContext, useReducer } from "react";
import Reducer from './Reducer'


const initialState = {
    openClose: false,
    openOpen: false,
    ticker: "BTCUSDT",
    tickerRows: [],
    positionRows: [],
    prices: {},
    walletRows: []
};

const Store = ({ children }) => {
    const [state, dispatch] = useReducer(Reducer, initialState);
    return (
        <Context.Provider value={[state, dispatch]}>
            {children}
        </Context.Provider>
    )
};

export const ACTIONS = {
    SET_CLOSE_OPEN: 'SET_CLOSE_OPEN',
    SET_OPEN_OPEN: 'SET_OPEN_OPEN',
    SET_TICKER: 'SET_TICKER',
    SET_TICKER_ROWS: 'SET_TICKER_ROWS',
    SET_POSITION_ROWS: 'SET_POSITION_ROWS',
    SET_PRICES: 'SET_PRICES',
    SET_WALLET_ROWS: 'SET_WALLET_ROWS'
}

export const Context = createContext(initialState);
export default Store;

Here is the reducer:

import { ACTIONS } from './Store'


const Reducer = (state, action) => {
    switch (action.type) {
        case ACTIONS.SET_CLOSE_OPEN:
            return {
                ...state,
                openClose: action.payload
            };
        case ACTIONS.SET_OPEN_OPEN:
            return {
                ...state,
                openOpen: action.payload
            };

...
        default:
            return state;
    }
};

export default Reducer;

I put the Store component in index.js so that it's context is available to all of the components in the app.

ReactDOM.render(
  <React.StrictMode>
    <Store>
      <App />
    </Store>
  </React.StrictMode>,
  document.getElementById('root')
);

Then when you want to access and update the state, you just import the actions and context and the useContext hook:

import { useContext} from "react";
import { ACTIONS, Context } from './store/Store';

and then you just add

const [state, dispatch] = useContext(Context);

inside your functional component and you can access the state and use dispatch to update it.

One limitation is that every component that accesses the state with useContext re-renders every time anything in the state gets updated, not just the part of the state that the component depends on. One way around this is to use the useMemo hook to control when the component re-renders.

export default function WalletTable({ priceDecimals }) {

  const classes = useStyles();

  const [state, dispatch] = useContext(Context);

  async function getCurrentRows() {
    const response = await fetch("http://localhost:8000/wallet");
    const data = await response.json();
    dispatch({ type: ACTIONS.SET_WALLET_ROWS, payload: data });
  }

  useEffect(() => {
    getCurrentRows();
  }, []);

  const MemoizedWalletTable = useMemo(() => {
    return (
      <TableContainer component={Paper}>
...
      </TableContainer>
    );
  }, [state.walletRows])

  return MemoizedWalletTable
}

Having to memoize everything makes it seem like maybe just using redux isn't all that much more complicated and is easier to deal with once set up.

Upvotes: 1

Related Questions