Home > database >  React - Update non stateful data inside reducer
React - Update non stateful data inside reducer

Time:12-27

I am implementing a context that manages all the messages of a conversation.

To reduce the complexity of my algorithm, I have decided to use a Map "sectionsRef" for accessing some stuff in O(1).

This map, needs to be updated inside my reducer's logic, where I update the stateful data, in order to synchronize both.

export function MessagesProvider({ children }) {
  const [messages, dispatch] = useReducer(messagesReducer, initialState);

  const sectionsRef = useMemo(() => new Map(), []);

  const addMessages = (messages, unshift = false) => {
    dispatch(actionCreators.addMessages(messages, unshift));
  };

  const addMessage = (message) => addMessages([message]);

  const deleteMessage = (messageId) => {
    dispatch(actionCreators.deleteMessage(messageId));
  };

  const value = useMemo(() => ({
    messages,
    addMessages,
    deleteMessage,
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }), [messages]);

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

As you can see, I am using useMemo when initializing the Map in order to prevent re-initializations due to re-renders.

Is it correct to pass it as a payload to my reducer actions?

const addMessages = (messages, unshift = false) => {
   dispatch(actionCreators.addMessages(messages, unshift, sectionsRef)); <---
};

To simplify my problem, imagine this is the real code:

//
// Reducer action
//

function reducerAction(state, messages, sectionsRef, title) {
  state.push(...messages);
  sectionsRef.set(title, state.length - 1);
}

//
// Context code
//

const state = [];
const firstMessagesSection = [{ id: 1 }];
const secondMessagesSection = [{ id: 1 }, { id: 2 }]
const sectionsRef = new Map();

reducerAction(state, firstMessagesSection, sectionsRef, "first section");
reducerAction(state, secondMessagesSection, sectionsRef, "second section");

console.log(state);
console.log(sectionsRef.get("second section"));

I am asking this because I have read that we shouldn't run side effects inside the reducers logic... so, if I need to synchronize that map with the state, what should I do instead?

CodePudding user response:

Is it correct to pass it as a payload to my reducer actions?

No: reducers must be pure functions.

Redux describes reducers using a short list which I think is very useful:

Rules of Reducers​

We said earlier that reducers must always follow some special rules:

  • They should only calculate the new state value based on the state and action arguments
  • They are not allowed to modify the existing state. Instead, they must make immutable updates, by copying the existing state and making changes to the copied values.
  • They must not do any asynchronous logic or other "side effects"

The second and third items together describe pure functions, and the first one is just a Redux-specific convention.

In your example, you are violating two rules of pure functions:

  • mutating state with state.push(...messages) (rather than creating a new array and returning it), and
  • performing side-effects by modifying a variable in the outer scope: sectionsRef.set(title, state.length - 1)

Further, you seem to never use the Map (how is it accessed in your program?). It should be included in your context, and you can simply define it outside your component (its identity will never change so it won't cause a re-render).

Here's how you can refactor your code to achieve your goal:

Keep the reducer data pure:

// store.js

export function messagesReduer (messages, action) {
  switch (action.type) {
    case 'ADD': {
      const {payload, unshift} = action;
      return unshift ? [...payload, ...messages] : [...messages, ...payload];
    }
    case 'DELETE': {
      const {payload} = action;
      return messages.filter(m => m.id !== payload);
    }
  }
}

export const creators = {};
creators.add = (messages, unshift = false) => ({type: 'ADD', payload: messages, unshift});
creators.delete = (id) => ({type: 'DELETE', payload: id});

export const sections = new Map();

Update the Map at the same that you dispatch an action to the related state by combining those operations in a function:

// MessagesContext.jsx

import {
  createContext,
  useCallback,
  useMemo,
  useReducer,
} from 'react';
import {
  creators,
  messagesReduer,
  sections,
} from './store';

export const MessagesContext = createContext();

export function MessagesProvider ({ children }) {
  const [messages, dispatch] = useReducer(messagesReducer, []);

  const addMessages = useCallback((title, messages, unshift = false) => {
    dispatch(creators.add(messages, unshift));
    sections.set(title, messages.length);
  }, [creators.add, dispatch, messages]);

  const addMessage = useCallback((title, message, unshift = false) => {
    dispatch(creators.add([message], unshift));
    sections.set(title, messages.length);
  }, [creators.add, dispatch, messages]);

  const deleteMessage = useCallback((id) => {
    dispatch(creators.delete(id));
  }, [creators.delete, dispatch]);

  const value = useMemo(() => ({
    addMessage,
    addMessages,
    deleteMessage,
    messages,
    sections,
  }), [
    addMessage,
    addMessages,
    deleteMessage,
    messages,
    sections,
  ]);

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

Use the context:

// App.jsx

import {useContext} from 'react';
import {MessagesContext, MessagesProvider} from './MessagesContext';

function Messages () {
  const {
    // addMessage,
    // addMessages,
    // deleteMessage,
    messages,
    // sections,
  } = useContext(MessagesContext);

  return (
    <ul>
      {
        messages.map(({id}, index) => (
          <li key={id}>Message no. {index   1}: ID {id}</li>
        ))
      }
    </ul>
  );
}

export function App () {
  return (
    <MessagesProvider>
      <Messages />
    </MessagesProvider>
  );
}

Additional notes:

  • Related