简体   繁体   中英

Re-rendering react component after clicking Like button (with Redux)

I have the following React component that shows all the users posts through the "renderPosts" method. Below it there's a like/unlike button on whether the currently logged in user has liked the post.

However, when I click on the like button, the component does not re-render in order for the "renderPosts" method to create an unlike button and the "like string" is modified as expected. Only when I go to another component and then come back to this component does the unlike button display and vice versa.

Is there anyway that I could fix this with Redux in my app? I tried this.forceUpdate after the onClick event but still does not work...

Also I tried creating a new Reducer called "likers", according to robinsax which basically get the array of users who like a particular post and imported it as props into the component but got

"this.props.likers.includes(currentUser)" is not a function

When the app first gets to the main page (PostIndex), probably because this.props.likers is still an empty object returned from reducer

Here is the code for my action creator:

export function likePost(username,postId) {
    // body...
    const request = {
        username,
        postId
    }
    const post = axios.post(`${ROOT_URL}/likePost`,request);
    return{
        type: LIKE_POST,
        payload: post
    }
}
export function unlikePost(username,postId){
    const request = {
        username,
        postId
    }
    const post = axios.post(`${ROOT_URL}/unlikePost`,request);
    return{
        type: UNLIKE_POST,
        payload: post
    }
}

And this is my reducer:

import {LIKE_POST,UNLIKE_POST} from '../actions/index.js';

export default function(state = {},action){
    switch(action.type){
        case LIKE_POST:
            const likers = action.payload.data.likedBy;
            console.log(likers);
            return likers;
        case UNLIKE_POST:
            const unlikers = action.payload.data.likedBy;
            console.log(unlikers);
            return unlikers;
        default:
            return state;
    }
}

I would really appreciate any help since I'm a beginner

import { fetchPosts } from "../actions/";
import { likePost } from "../actions/";
import { unlikePost } from "../actions/";
class PostsIndex extends Component {
    componentDidMount() {
        this.props.fetchPosts();
    }
    renderPost() {
        const currentUser = Object.values(this.props.users)[0].username;
        return _.map(this.props.posts, post => {
            return (
                <li className="list-group-item">
                    <Link to={`/user/${post.username}`}>
                        Poster: {post.username}
                    </Link>
                    <br />
                    Created At: {post.createdAt}, near {post.location}
                    <br />
                    <Link to={`/posts/${post._id}`}>{post.title}</Link>
                    <br />
                    //error here, with this.props.likers being an 
                    //array
                    {!this.props.likers.includes(currentUser) ? (
                        <Button
                            onClick={() => this.props.likePost(currentUser,post._id)}
                            bsStyle="success"
                        >
                            Like
                        </Button>
                    ) : (
                        <Button
                            onClick={() => this.props.unlikePost(currentUser,post._id)}
                            bsStyle="warning"
                        >
                            Unlike
                        </Button>
                    )}{" "}
                    {post.likedBy.length === 1
                        ? `${post.likedBy[0]} likes this`
                        : `${post.likedBy.length} people like this`}
                </li>
            );
        });
    }
function mapStateToProps(state) {
    return {
        posts: state.posts,
        users: state.users,
        likers: state.likers
    };
}
}

You need to use redux-thunk middleware in order to use async actions. First, add redux-thunk while creating store like

import thunk from 'redux-thunk';
const store = createStore(
   rootReducer,
   applyMiddleware(thunk)  
);

then change your method like this

export function likePost(username,postId) {
    return function(dispatch) {
        // body...
         const request = {
            username,
            postId
          }
         axios.post(`${ROOT_URL}/likePost`,request)
          .then(res => {
              dispatch({
                 type: LIKE_POST,
                 payload: res
           });
      });
    }
}

and now in your component after mapStateToProps, define mapDispatchToProps,

const mapDispatchToProps = dispatch => {
       return {
           likePost: (currentUser,postId) => dispatch(likePost(currentUser, postId)),
           // same goes for "unlike" function
       }
 }

export default connect(mapStateToProps, mapDispatchToProps)(PostsIndex);

Seems like the like/unlike post functionality isn't causing anything in your state or props to change, so the component doesn't re-render.

You should change the data structure you're storing so that the value of post.likedBy.includes(currentUser) is included in one of those, or forceUpdate() the component after the likePost and unlikePost calls.

Please do it the first way so I can sleep at night. Having a component's render() be affected by things not in its props or state defeats the purpose of using React .

The problem is in your action creator.

export function likePost(username,postId) {
    // body...
    const request = {
        username,
        postId
    }
    // this is an async call
    const post = axios.post(`${ROOT_URL}/likePost`,request);
    // next line will execute before the above async call is returned
    return{
        type: LIKE_POST,
        payload: post
    }
}

Because of that your state is likely never updated and stays in the initial value.

You would need to use either redux-thunk or redux-saga to work with async actions.

As noted in other answers, you need to use redux-thunk or redux-saga to make async calls that update you reducer. I personally prefer redux-saga . Here's is a basic implementation of React, Redux, and Redux-Saga.

Redux-Saga uses JavaScript generator functions and yield to accomplish the goal of handling async calls.

Below you'll see a lot of familiar React-Redux code, the key parts of Redux-Saga are as follows:

  • watchRequest - A generator function that maps dispatch actions to generator functions
  • loadTodo - A generator function called from watchRequest to yield a value from an async call and dispatch an action for the reducer
  • getTodoAPI - A regular function that makes a fetch request
  • applyMiddleware - from Redux is used to connect Redux-Saga with createStore

 const { applyMiddleware, createStore } = Redux; const createSagaMiddleware = ReduxSaga.default; const { put, call } = ReduxSaga.effects; const { takeLatest } = ReduxSaga; const { connect, Provider } = ReactRedux; // API Call const getTodoAPI = () => { return fetch('https://jsonplaceholder.typicode.com/todos/1') .then(response => { return response.json() .then(response => response); }) .catch(error => { throw error; }) }; // Reducer const userReducer = (state = {}, action) => { switch (action.type) { case 'LOAD_TODO_SUCCESS': return action.todo; default: return state; } }; // Sagas, which are generator functions // Note: the asterix function* loadTodo() { try { const todo = yield call(getTodoAPI); yield put({type: 'LOAD_TODO_SUCCESS', todo}); } catch (error) { throw error; } } // Redux-Saga uses generator functions, // which are basically watchers to wait for an action function* watchRequest() { yield* takeLatest('LOAD_TODO_REQUEST', loadTodo); } class App extends React.Component { render() { const { data } = this.props; return ( <div> <button onClick={() => this.props.getTodo()}>Load Data</button> {data ? <p>data: {JSON.stringify(data)}</p> : null } </div> ) } } // Setup React-Redux and Connect Redux-Saga const sagaMiddleware = createSagaMiddleware(); const store = createStore(userReducer, applyMiddleware(sagaMiddleware)); sagaMiddleware.run(watchRequest); // Your regular React-Redux stuff const mapStateToProps = (state) => ({ data: state }); // Map the store's state to component's props const mapDispatchToProps = (dispatch) => ({ getTodo: () => dispatch({type: 'LOAD_TODO_REQUEST'}) }) // wrap action creator with dispatch method const RootComponent = connect(mapStateToProps, mapDispatchToProps)(App); ReactDOM.render( <Provider store={store}> <RootComponent /> </Provider>, document.getElementById('root') ); 
 <script src="https://npmcdn.com/babel-regenerator-runtime@6.3.13/runtime.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.1/redux.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/6.0.0/react-redux.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/redux-saga/0.16.2/redux-saga.min.js"></script> <div id="root"></div> 

As they say use redux-thunk or redux-saga. If your new to redux I prefer redux-thunk because it's easy to learn than redux-saga. You can rewrite your code like this

export function likePost(username,postId) {
// body...
const request = {
    username,
    postId
}
const post = axios.post(`${ROOT_URL}/likePost`,request);
return dispatch => {
    post.then(res => {
      dispatch(anotherAction) //it can be the action to update state
    });
}

}

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