Josh Simon
Josh Simon

Reputation: 259

useSelector only returning initial state

I'm building a simple review app with react and redux toolkit.

Reviews are added via a form in AddReview.js, and I'm wanting to display these reviews in Venue.js.

When I submit a review in AddReview.js, the new review is added to state, as indicated in redux dev tools:

enter image description here

However when I try to pull that state from the store in Venue.js, I only get the initial state (the first two reviews), and not the state I've added via the submit form:

enter image description here

Can anyone suggest what's going wrong here?

Here's how I've set up my store:

store.js

import { configureStore } from "@reduxjs/toolkit";
import reviewReducer from '../features/venues/venueSlice'

export const store = configureStore({
    reducer:{
       reviews: reviewReducer
    }
})

Here's the slice managing venues/reviews:

venueSlice.js

import { createSlice } from "@reduxjs/toolkit";

const initialState = [
    {id:1, title: 'title 1',blurb: 'blurb 1'},
    {id:2, title: 'title 2',blurb: 'blurb 2'}
]

const venueSlice = createSlice({
    name: 'reviews',
    initialState,
    reducers: {
        ADD_REVIEW: (state,action) => {
            state.push(action.payload)
        }
    }
})

export const { ADD_REVIEW } = venueSlice.actions

export default venueSlice.reducer

And here's the Venue.js component where I want to render reviews:

import { useParams } from "react-router-dom";
import { useSelector } from "react-redux";

const Venue = () => {
    const { id } = useParams()

    const reviews = useSelector((state) => state.reviews)

    console.log(reviews)

    return (
        <div>
            {reviews.map(item => (
                <h1>{item.title}</h1>
            ))}
        </div>
    )
}
 
export default Venue;

Form component AddReview.js

import { useState } from "react"
import { useDispatch } from "react-redux"
import { ADD_REVIEW } from "./venueSlice"
import { nanoid } from "@reduxjs/toolkit"

const AddReview = () => {
    const [ {title,blurb}, setFormDetails ] = useState({title:'', blurb: ''})

    const dispatch = useDispatch()

    const handleChange = (e) => {
        const { name, value } = e.target
        setFormDetails(prevState => ({  
            ...prevState,
            [name]: value
        }))
    }

    const handleSubmit = (e) => {
        console.log('it got here')
        e.preventDefault()
        if(title && blurb){
            dispatch(ADD_REVIEW({
                id: nanoid(),
                title,
                blurb
            }))
        // setFormDetails({title: '', blurb: ''})
        }
    }

    return(
        <div>
            <form onSubmit={handleSubmit}>
                <input
                type = 'text'
                name = 'title'
                onChange={handleChange}
                />
                <input
                type = 'text'
                name = 'blurb'
                onChange={handleChange}
                />
                <button type = "submit">Submit</button>
            </form>
        </div>
    )
}
 
export default AddReview;

Upvotes: 1

Views: 1174

Answers (3)

Berci
Berci

Reputation: 3386

Expanding on @electroid answer (the solution he provided should fix your issue and here is why):

Redux toolkit docs mention on Rules of Reducers :

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.

and on Reducers and Immutable Updates :

One of the primary rules of Redux is that our reducers are never allowed to mutate the original / current state values!

And as mdn docs specify the push method changes the current array (so it mutates your state). You can read more about mutating the state in the second link link (Reducers and Immutable Updates).

If you really want to keep the state.reviews and avoid state.reviews.reviews you could also do something like this:

        ADD_REVIEW: (state,action) => {
            state = [...state, action.payload];
        }

But I wouldn't recommend something like this in a real app (it is avoided in all the examples you can find online). Some reason for this would be:

  1. It harder to work with, read and track the state when having an overall dynamic state instead of a state structure
  2. It leads to a lot of slices in a real app (creating a slices for an array without grouping the data) which can also become hard to track and maintain.
  3. Usually you need a redux slice in multiple parts of the app (otherwise you can just use state). That data is usually bigger than just an array and not grouping the data properly on reducers can become very very confusing.

But I would definitely advise to use something else (not reviews.reviews). In your case I think something like state.venue.reviews (so on store.js

...
export const store = configureStore({
    reducer:{
       venue: reviewReducer // reviewReducer should probably also be renamed to venueSlice or venueReducer
    }
})

So an option to avoid state.venue.reviews or state.reviews.reviews would be to export a selector from the venueSlice.js:

export const selectReviews = (state) => state.venue.reviews

and in your Venue.js component you can just use

const reviews = useSelector(selectReviews)

Exporting a selector is actually suggested by the redux toolkit tutorials as well (this link is for typescript but the same applies to javascript). Although this is optional.

Upvotes: 0

electroid
electroid

Reputation: 661

I can notice that you pushing directly to the state, I can suggest to use variable in the state and then modify that variable. Also I suggest to use concat instead of push. Where push will return the array length, concat will return the new array. When your code in the reducer will looks like that:

import { createSlice } from "@reduxjs/toolkit";

const initialState = [
    reviews: [{id:1, title: 'title 1',blurb: 'blurb 1'},
             {id:2, title: 'title 2',blurb: 'blurb 2'}]
]

const venueSlice = createSlice({
    name: 'reviews',
    initialState,
    reducers: {
        ADD_REVIEW: (state,action) => {
            state.reviews = state.reviews.concat(action.payload);
        }
    }
})

export const { ADD_REVIEW } = venueSlice.actions

export default venueSlice.reducer

And then your selector will looks like that:

const reviews = useSelector((state) => state.reviews.reviews)

Upvotes: 1

Manish Kumar
Manish Kumar

Reputation: 1176

Your code seems to be fine. I don't see any reason why it shouldn't work. I run your code on stackblitz react template and its working as expected.

Following is the link to the app: stackblitz react-redux app

Link to the code: Project files react-redux

if you are still unable to solve the problem, do create the sandbox version of your app with the issue to help further investigate.

Thanks

Upvotes: 0

Related Questions