Home > Mobile >  Advise about the reduction of the amount of React useState hooks
Advise about the reduction of the amount of React useState hooks

Time:11-05

I am trying to build my very own first project that is relatively large, at least for my level of experience anyway. I am heavily relying on useContext in combination with useStates hooks to handle the logic between my different components, as time goes, it's really starting to get difficult to keep track of all these different states changes and simple onClick events I have to change the logic of a large number of states.

Hoping for a little bit of personal advice that could steer me in the right direction. as somehow what I do, doesn't feel normal, or this is the reality of React? Surely there are more clever ways to reduce the amount of state logic management?

Here are a few snippets of code I am using

  const onClick = (note: INote) => {
    SetAddNote(false);
    SetNote(note);
    onSelected(note)
    SetReadOnly(true);
    SetEditor(note.data.value);
    SetInputValue(note.data.name);
    SetCategory(note.data.category);
  };
  const { note, noteDispatch, SetNoteDispatch } = useContext(NoteContext);
  const { categories } = useContext(CategoriesContext);
  const [ editMode, setEditMode ] = useState(false);
  const [ module, setModule ] = useState<{}>(modulesReadOnly)
  const [inputValue, setInputValue] = useState<string>('');
  const [category, setCategory] = useState('');
  const [color, setColor] = useState('');
import React, { createContext, useState } from 'react';


type EditorContextType = {
  editor: string;
  SetEditor: React.Dispatch<React.SetStateAction<string>>;
  readOnly: boolean;
  SetReadOnly: React.Dispatch<React.SetStateAction<boolean>>;
  inputValue: string;
  SetInputValue: React.Dispatch<React.SetStateAction<string>>;
  category: string;
  SetCategory: React.Dispatch<React.SetStateAction<string>>;
};

type EditorContextProviderProps = {
  children: React.ReactNode;
};

export const EditorContext = createContext({} as EditorContextType);

export const EditorContextProvider = ({
  children,
}: EditorContextProviderProps) => {
  const [editor, SetEditor] = useState('');
  const [readOnly, SetReadOnly] = useState(false)
  const [inputValue, SetInputValue] = useState('');
  const [category, SetCategory] = useState('');
  return (
    <EditorContext.Provider value={{ editor, SetEditor, readOnly, SetReadOnly, inputValue, SetInputValue, category, SetCategory  }}>
      {children}
    </EditorContext.Provider>
  );
};

Sure I could shave a few states and merge them into one, but seems like that would get even more complex than it is.

I am reading about the useReducer hook however it's difficult to grasp the entire idea behind it yet and not quite sure if it's really going to help me in this case. It feels to me I am setting myself for a failure given I continue working in this fashion, but I don't see any better options to implement

CodePudding user response:

I have working on big project too, and as you say in your question, Reducer will help you to fix your issue, but surly you need to be careful how you will build and manage your state, so the idea how you manage a state, so before I put my answer, I will write some important note:

  1. Make sure to reduce nested context as you can, only build context and use context when theres a needed for that, this will optomize your work
  2. For handling or merge state, you can use object, arrays and normal variable, but keep in your mind, try to prevent nested level of objects, to keep state update on state change.
  3. Use reducer to handling update on state will give you a nice ability
  4. We can do some tricks to improve performance like set condition in reducer which its check old state and new state.

Keep in your mind, really its easy to use it, but the first time its hard to learn...

Now lets start from a real project example:

// create VirtualClass context
export const JitsiContext = React.createContext();

// General Action
const SET_IS_SHARED_SCREEN = 'SET_IS_SHARED_SCREEN';
const SET_ERROR_TRACK_FOR_DEVICE = 'SET_ERROR_TRACK_FOR_DEVICE';
const UPDATE_PARTICIPANTS_INFO = 'UPDATE_PARTICIPANTS_INFO';
const UPDATE_LOCAL_PARTICIPANTS_INFO = 'UPDATE_LOCAL_PARTICIPANTS_INFO';

// Initial VirtualClass Data
const initialState = {
  errorTrackForDevice: 0,
  participantsInfo: [],
  remoteParticipantsInfo: [],
  localParticipantInfo: {}
};

// Global Reducer for handling state
const Reducer = (jitsiState = initialState, action) => {
  switch (action.type) {
    case UPDATE_PARTICIPANTS_INFO:// Update particpants info and remote list
      if (arraysAreEqual(action.payload, jitsiState.remoteParticipantsInfo)) {
        return jitsiState;
      }

      return {
        ...jitsiState,
        participantsInfo: [jitsiState.localParticipantInfo, ...action.payload],
        remoteParticipantsInfo: action.payload,
      };
    case UPDATE_LOCAL_PARTICIPANTS_INFO:// Update particpants info and local one
      if (JSON.stringify(action.payload) === JSON.stringify(jitsiState.localParticipantInfo)) {
        return jitsiState;
      }

      return {
        ...jitsiState,
        localParticipantInfo: action.payload,
        participantsInfo: [action.payload, ...jitsiState.remoteParticipantsInfo],
      };
    case SET_IS_SHARED_SCREEN:
      if (action.payload === jitsiState.isSharedScreen) {
        return jitsiState;
      }

      return {
        ...jitsiState,
        isSharedScreen: action.payload,
      };
    default:
      throw new Error(`action: ${action.type} not supported in VirtualClass Context`);
  }
};


const JitsiProvider = ({children}) => {
  const [jitsiState, dispatch] = useReducer(Reducer, initialState);
  
  // Update shared screen flag
  const setIsSharedScreen = useCallback((flag) => {
    dispatch({type: SET_IS_SHARED_SCREEN, payload: flag})
  }, []);

  // Update list of erros
  const setErrorTrackForDevice = useCallback((value) => {
    dispatch({type: SET_ERROR_TRACK_FOR_DEVICE, payload: value})
  }, []);
  
  // Local Participant info
  const updateLocalParticipantsInfo = useCallback((value) => {
    dispatch({type: UPDATE_LOCAL_PARTICIPANTS_INFO, payload: value})
  }, []);
  
  const updateParticipantsInfo = useCallback(async (room, currentUserId = null) => {
    if (!room.current) {
      return;
    }

    // get current paricipants in room
    let payloads = await room.current.getParticipants();
    // ... some logic
    let finalResult = payloads.filter(n => n).sort((a, b) => (b.startedAt - a.startedAt));
    dispatch({type: UPDATE_PARTICIPANTS_INFO, payload: finalResult})
  }, []);

  const contextValue = useMemo(() => {
    return {
      jitsiState,
      setIsSharedScreen,
      setErrorTrackForDevice,
      updateParticipantsInfo,
      updateLocalParticipantsInfo,
    };
  }, [jitsiState]);

  return (
    <JitsiContext.Provider
      value={contextValue}
    >
      {children}
    </JitsiContext.Provider>
  );
};

export default JitsiProvider;

This example allow you to update state and you have more than one case, all state value share by jitsiState, so you can get any data you want, and about function, you can use dispatch direct! but in our experience we build a callback method and send it via provider too, this give us upillty to control code and logic in one place, and make process very easy, so when click in every place just we call needed method...

You will see also conditions and useMemo...these to prevent render un-needed trigger, like change the key in memory not the real value and so on...

Finally after we use it, we control now all the state between all component too easy, and we didn't have nested context except the wrapper context.

Note: surly you can skip or change this code base on your logic or needed concepts.

Note 2: this code is catted and do some changes to make it easy to read or understand...

Note 3: You can ignore all functions pass in provider and use dispatch direct, but in my project I send a function like this example.

  • Related