简体   繁体   中英

How to stop re-rendering a whole list of items when only one item of the list is created or updated in ReactJs React-Redux?

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!

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>

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM