4

I am making this web applications which has posts where users can put answers to those posts. I have used React-Redux to manage the state of the application. Every time I create or update an answer of a particular post the whole list of answers which belongs to that post gets re-rendered and I want to stop that and render only the newly created or updated one. I have used exactly the same way for post comments and it works fine. Comments doesn't get re-rendered but answers does. I just can't figure out what is the problem here. Please refer the code below.

I tried using React.memo() also and it doesn't work either!

Answer render component,

export function Answer() {
    const classes = useStyles();
    const dispatch = useDispatch();

    const { postId } = useParams();

    const postAnswers = useSelector(state => state.Answers);

    const [answers, setAnswers] = React.useState(postAnswers.answers);

    React.useEffect(() => {
        if(postAnswers.status === 'idle') dispatch(fetchAnswers(postId));
    }, [dispatch]);

    React.useEffect(() => {
        if(postAnswers.answers) handleAnswers(postAnswers.answers);
    }, [postAnswers]);

    const handleAnswers = (answers) => {
        setAnswers(answers);
    };

    const AnswersList = answers ? answers.map(item => {

        const displayContent = item.answerContent;

        return(
            <Grid item key={item.id}> 
                <Grid container direction="column">
                    <Grid item>
                        <Paper component="form" className={classes.root} elevation={0} variant="outlined" >
                            <div className={classes.input}>
                                <Typography>{displayContent}</Typography>
                            </div>
                        </Paper>
                    </Grid>
                </Grid>
            </Grid>
        );
    }): undefined;

    return(
        <Grid container direction="column" spacing={2}>
            <Grid item>
                <Divider/>
            </Grid>
            <Grid item> 
                <Grid container direction="column" alignItems="flex-start" justify="center" spacing={2}>
                    {AnswersList}
                </Grid>
            </Grid>
            <Grid item>
                <Divider/>
            </Grid>
        </Grid>
    );
}

Fetch answers redux apply,

export const fetchAnswers = (postId) => (dispatch) => {
    dispatch(answersLoading());

    axios.get(baseUrl + `/answer_api/?postBelong=${postId}`)
    .then(answers => 
        dispatch(addAnswers(answers.data))
    )
    .catch(error => {
        console.log(error);
        dispatch(answersFailed(error));
    });
}

Post answers,

export const postAnswer = (data) => (dispatch) => {
    axios.post(baseUrl + `/answer_api/answer/create/`,
        data
    )
    .then(response => {
        console.log(response);
        dispatch(fetchAnswers(postBelong)); //This is the way that I update answers state every time a new answer is created or updated
    })
    .catch(error => {
        console.log(error);
    });
}

Any help would be great. Thank you!

2 Answers2

1

After adding an item you fetch all the items from the api so all items are recreated in the state. If you give a container component the id of the item and have the selector get the item as JSON then parse back to object you can memoize it and prevent re render but I think it's probably better to just re render.

Here is an example of memoized JSON for the item:

const { Provider, useDispatch, useSelector } = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
const { createSelector } = Reselect;
const fakeApi = (() => {
  const id = ((num) => () => ++num)(1);
  const items = [{ id: 1 }];
  const addItem = () =>
    Promise.resolve().then(() =>
      items.push({
        id: id(),
      })
    );
  const updateFirst = () =>
    Promise.resolve().then(() => {
      items[0] = { ...items[0], updated: id() };
    });
  const getItems = () =>
    //this is what getting all the items from api
    //  would do, it re creates all the items
    Promise.resolve(JSON.parse(JSON.stringify(items)));
  return {
    addItem,
    getItems,
    updateFirst,
  };
})();
const initialState = {
  items: [],
};
//action types
const GET_ITEMS_SUCCESS = 'GET_ITEMS_SUCCESS';
//action creators
const getItemsSuccess = (items) => ({
  type: GET_ITEMS_SUCCESS,
  payload: items,
});
const getItems = () => (dispatch) =>
  fakeApi
    .getItems()
    .then((items) => dispatch(getItemsSuccess(items)));
const update = () => (dispatch) =>
  fakeApi.updateFirst().then(() => getItems()(dispatch));
const addItem = () => (dispatch) =>
  fakeApi.addItem().then(() => getItems()(dispatch));
const reducer = (state, { type, payload }) => {
  if (type === GET_ITEMS_SUCCESS) {
    return { ...state, items: payload };
  }
  return state;
};
//selectors
const selectItems = (state) => state.items;
const selectItemById = createSelector(
  [selectItems, (_, id) => id],
  (items, id) => items.find((item) => item.id === id)
);
const createSelectItemAsJSON = (id) =>
  createSelector(
    [(state) => selectItemById(state, id)],
    //return the item as primitive (string)
    (item) => JSON.stringify(item)
  );
const createSelectItemById = (id) =>
  createSelector(
    [createSelectItemAsJSON(id)],
    //return the json item as object
    (item) => JSON.parse(item)
  );
//creating store with redux dev tools
const composeEnhancers =
  window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
  reducer,
  initialState,
  composeEnhancers(
    applyMiddleware(
      ({ dispatch, getState }) => (next) => (action) =>
        //simple thunk implementation
        typeof action === 'function'
          ? action(dispatch, getState)
          : next(action)
    )
  )
);
const Item = React.memo(function Item({ item }) {
  const rendered = React.useRef(0);
  rendered.current++;
  return (
    <li>
      rendered:{rendered.current} times, item:{' '}
      {JSON.stringify(item)}
    </li>
  );
});
const ItemContainer = ({ id }) => {
  const selectItem = React.useMemo(
    () => createSelectItemById(id),
    [id]
  );
  const item = useSelector(selectItem);
  return <Item item={item} />;
};
const ItemList = () => {
  const items = useSelector(selectItems);
  return (
    <ul>
      {items.map(({ id }) => (
        <ItemContainer key={id} id={id} />
      ))}
    </ul>
  );
};
const App = () => {
  const dispatch = useDispatch();
  React.useEffect(() => dispatch(getItems()), [dispatch]);
  return (
    <div>
      <button onClick={() => dispatch(addItem())}>
        add item
      </button>
      <button onClick={() => dispatch(update())}>
        update first item
      </button>
      <ItemList />
    </div>
  );
};

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reselect/4.0.0/reselect.min.js"></script>
<div id="root"></div>
HMR
  • 37,593
  • 24
  • 91
  • 160
  • Thanks a lot for the answer and why do you think it's better to re-render the list of answers every time one answer is created or updated? Does it affect the quality of the application? or is there any other way to do that? – Janitha Nawarathna Dec 10 '20 at 02:42
  • @janithahn If you are not rendering a lot then it would make the code a lot simpler. But if you notice a performance problem you could try the memoized selector used in my answer. – HMR Dec 10 '20 at 07:42
  • Ah okay. Got it! Thank you very much! – Janitha Nawarathna Dec 10 '20 at 14:43
1

I just found where the problem was which led to the above question. In my state management system there is an action named answers to handle the state of post answers like below.

import * as ActionTypes from '../ActionTypes';

export const Answers = (state = {
        status: 'idle',
        errMess: null,
        answers: []
    }, action) => {
    switch(action.type) {

        case ActionTypes.ADD_ANSWER_LIST:
            return {...state, status: 'succeeded', errMess: null, answers: action.payload}

        case ActionTypes.ANSWER_LIST_LOADING:
            return {...state, status: 'loading', errMess: null, answers: []}
        
        case ActionTypes.ANSWER_LIST_FAILED:
            return {...state, status: 'failed', errMess: action.payload, answers: []}

        default:
            return state;
    }
}

The problem here is that the empty arrays that I have put in ANSWER_LIST_LOADING and ANSWER_LIST_FAILED cases. Every time the action creator fetches new data, it goes through the loading state and there it gets an empty array which leads the whole list of answers to be re-rendered and re-created unnecessarily. So I changed the implementation as follows and it fixed the problem.

export const Answers = (state = {
        status: 'idle',
        errMess: null,
        answers: []
    }, action) => {
    switch(action.type) {

        case ActionTypes.ADD_ANSWER_LIST:
            return {...state, status: 'succeeded', errMess: null, answers: action.payload}

        case ActionTypes.ANSWER_LIST_LOADING:
            return {...state, status: 'loading', errMess: null, answers: [...state.answers]}
        
        case ActionTypes.ANSWER_LIST_FAILED:
            return {...state, status: 'failed', errMess: action.payload, answers: [...state.answers]}

        default:
            return state;
    }
}

All the time the problem has been in a place where I never thought it would be. I haven't even mentioned about this action in my question. But there you go.