3

I have a parent-child component Modal and ModalContent in my React App both functional.

1) I have created an AppContext in App.js for accessing it globally in all components.

const [state, dispatch] = useReducer(reducer, {modalOpen: false, content: []});

    <AppContext.Provider value={{ state, dispatch }} >
      //BrowserRouter and Routes Definition
    </App>

Here reducer is a fairly plain function with toggle for modalOpen and push/pop functionality in content(array).

2) My Modal Component makes use of

const { state, dispatch } = useContext(AppContext); 
<Modal open={state.modalOpen} />

to get the state of modal visibility to set it open/closed.

3) My ModalContent Component makes use of

const { state, dispatch } = useContext(AppContext); 
<ModalContent data={state.content} />
  //Dispatch from the `ModalContent`
  dispatch({ type: 'UPDATE_CONTENT', data: 'newata' });

4) Here is my reducer.

export const reducer = (state, action) => {
switch (action.type) {
    case 'TOGGLE_MODAL':
            return {...state, modalOpen: !state.modalOpen};
    case 'UPDATE_CONTENT':
        return { ...state, content: [...state.content, action.data]};
    default:
        return {modalOpen: false,content: []};
 }
} 

I have set up some code in ModalContent to update the data content property using dispatch and the reducer store is updated perfectly and returns fresh/updated:

 {modalOpen: true/false, content: updatedContentArray}

The issue is: Whenever I dispatch an action via ModalContent complete state is returned(expected), by the reducer and Modal reopens itself as it listens to state.modalOpen.

Unsuccessful Attempt: I attempted to specifically pull required properties in respective components. But the Modal component still rerenders even when just content is changed. Is there any way to watch for specific state only

How can make this work by rerendering only ModalContent not Modal.

Edit: Updated question with my mock(working) reducer code and dispatch statement from the ModalContent itself.

nikhil024
  • 435
  • 5
  • 19
  • 1
    can you show your reducer and the dispatch call. Also is the issue just that Modal is re-rendering or that the openModal is incorrectly getting modelOpen value as true – Shubham Khatri May 30 '20 at 13:37
  • You could enhance `Modal` with `React.memo` to be a memoized component, which won't re-render, if `state.modalOpen` has the same value. – ford04 May 30 '20 at 14:00
  • @ShubhamKhatri I have added my simpler version of my reducer(working) and added a dispatch call in the code snippet above. The openModal is working fine as I can see when I close the modal it closes and state updates to false for that. – nikhil024 May 30 '20 at 14:08
  • ok, so the issue is just the re-rendering of Modal component when only content changes? – Shubham Khatri May 30 '20 at 14:10
  • Yes @ShubhamKhatri – nikhil024 May 30 '20 at 14:14

2 Answers2

3

The reason both Modal and ModalContent get re-rendered when Content changes is because both the components make use of the same context and when a context value changes all components listening to the context are re-rendered

A way to fix this re-rendering thing is to make use of multiple contexts like

 const modalContextVal = useMemo(() => ({ modalOpen: state.modalOpen, dispatch}), [state.modalOpen]);
   const contentContextVal = useMemo(() => ({ content: state.content, dispatch}), [state.content]);
   ....
   <ModalContext.Provider value={modalContextVal}>
      <ContentContext.Provider value={contentContextVal}>
      //BrowserRouter and Routes Definition
      </ContentContext.Provider>
    </ModalContext.Provider>

And use it like

In Modal.js

const {modalOpen, dispatch} = useContext(ModalContext);

In ModalContent.js

const {content, dispatch} = useContext(ContentContext);
Shubham Khatri
  • 270,417
  • 55
  • 406
  • 400
  • 1
    Perfect @shubham, worked like a charm in fact with this approach I can even access multiple contexts in a single component(if needed) without worrying about rerendering until I specifically update that piece of state! – nikhil024 May 30 '20 at 19:09
1

As @Shubham said you have to keep modal state and modal content separate.

It can be done with separate context or even simple useState

Example snippet

const { useReducer, createContext, useContext, useState, useEffect, memo, useMemo } = React;

const AppContext = createContext();

const reducer = (state, action) => {
  if(action.type == 'toggleModal') {
    return {
      ...state,
      modalOpen: !state.modalOpen
    }
  }
  
  return state;
}

const AppContextProvider = ({children}) => {
  const [state, dispatch] = useReducer(reducer, {modalOpen: false, content: [{id: 1, value: 'test'}]});

  return <AppContext.Provider value={{state, dispatch}}>
    {children}
  </AppContext.Provider>
}

const Modal = ({children, modalOpen}) => {
  const { state, dispatch } = useContext(AppContext); 

  console.log('Render Modal');

  return <div className={`modal ${modalOpen ? 'modal--open': null}`}>
    {children}
  </div>
}

const ModalContent = ({data, onClick}) => {

  console.log('Render Modal Content');
  
  return <div className="modal__content">
    {data.map(({id, value}) => <div className="item" key={id}>{value}</div>)}
    <button onClick={onClick} className="modal__close">Close</button>
  </div>
}

const App = () => {
  const { state, dispatch } = useContext(AppContext); 
  const { modalOpen, } = state;
  const [content, setContent] = useState([]);
  
  const onClick = () => {
    dispatch({ type: 'toggleModal' });
  }

  return <div>
    <Modal modalOpen={modalOpen}>
      {useMemo(() => {
  
  console.log('render useMemo');
  
  return <ModalContent onClick={onClick} data={content}></ModalContent>
  }, [content])}
    </Modal>
    <button onClick={onClick}>Open Modal</button>
  </div>
}

ReactDOM.render(
    <AppContextProvider>
      <App />
    </AppContextProvider>,
    document.getElementById('root')
  );
.modal {
  background: black;
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  z-index: -1;
  transition: all .3s ease-in;
}

.modal__close {
  padding: .5rem 1rem;
  color: white;
  border: 1px solid white;
  background: transparent;
  cursor: pointer;
}

.modal--open {
  opacity: 1;
  z-index: 1;
}

.item {
  padding: 1rem;
  color: white;
  border: 1px solid white;
}
<script src="https://unpkg.com/react/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
<div id="root"></div>
Józef Podlecki
  • 10,453
  • 5
  • 24
  • 50