Understanding React Redux Reducer and MapstateToProps - reactjs

I'm having trouble understanding how the redux state assigns the state objects based on the action payload and the reducer functions. Below is my sample code. I've made notes and asked questions along the different sections, but in summary these are my questions:
Why does Option 2 below not work?
Why do I have to map my state to my competitionList prop using state.competitions and not state.items?
Any resources to get a good grasp of how react and redux connect and mapping functions work. I've already gone through the official docs and done some googling, but perhaps someone has a reference that they found easier to understand all the different options and ways of mapping state and dispatch.
My Action code:
function getAll() {
return dispatch => {
dispatch(request());
myService.getAll()
.then(
competitions => dispatch(success(competitions)),
error => dispatch(failure(error))
);
};
function request() { return { type: constants.GETALL_REQUEST } }
function success(competitions) { return {type: constants.GETALL_SUCCESS, competitions}}
function failure(error) { return {type: constants.GETALL_FAILURE, error}}
}
My reducer code:
import { constants } from '../_constants';
const initialState = {items: [], loading: false, selected: null}
export function competitions(state = initialState, action) {
switch (action.type) {
case constants.GETALL_REQUEST:
return {
loading: true
};
case constants.GETALL_SUCCESS:
console.log("the action value: ", action)
return {
items: action.competitions
};
case constants.GETALL_FAILURE:
console.log("the failed action value: ", action)
return {
error: action.error
};
default:
return state
}
}
In my component I have a mapStateToProp function which I pass to connect. The first one does not work. Why?
Option 1 - Not working
function mapStateToProps(state) {
const { selected, ...competitions } = state.competitions;
return {
competitionList: competitions,
isLoading: state.loading
};
}
export default connect(mapStateToProps)(Dashboard);
This one works, but I would like the competitionList variable to have the returned items array instead of the whole state object, so I tried to do something like this competition: state.competitions.items but it raises an error.
Option 2 - Partially working (I want to only assign the competition items)
const mapStateToProps = (state) => ({
competitionList: state.competitions,
isLoading: state.loading
});
export default connect(mapStateToProps)(Dashboard);
I cannot do:
const { competitionList } = this.props;
{competitionList.map(competition =>
<tr key={competition.competitionId}>
<td>{competition.competitionName}</td>
</tr>
)}
I have to do:
const { competitionList } = this.props;
{competitionList.items.map(competition =>
<tr key={competition.competitionId}>
<td>{competition.competitionName}</td>
</tr>
)}

I think the point that you are missing is when you combine your reducers, each one will have a key because they are objects.
In the file you combine your reducers, you probably have something like that:
import { combineReducers } from 'redux'
import todos from './todos'
import competitions from './competitions'
export default combineReducers({
todos,
competitions
})
After that, your state will look like this:
{
todos:{},
competitions:{
items: [],
loading: false,
selected: null
}
}
Explained that I think everything will be easier.
Option 1 - Not working: It is not working because you don't havecompetitions attribute inside the competitions state. Even if you have, you should not use the ... before it. If you replace the competitions for items, it is going to work, because items are inside the competitions state:
function mapStateToProps(state) {
const { selected, items } = state.competitions;
return {
competitionList: items,
isLoading: state.loading
};
}
export default connect(mapStateToProps)(Dashboard);
Or we can improve it, to make it shorter:
function mapStateToProps(state) {
const { selected, items } = state.competitions;
return {
items,
selected
isLoading: state.loading
};
}
export default connect(mapStateToProps)(Dashboard);
Doing this way, you can use this part of your code:
const { items } = this.props;
{items.map(competition =>
<tr key={competition.competitionId}>
<td>{competition.competitionName}</td>
</tr>
)}
There is another point I would like to point, Probably your isLoading variable is not working either, because you are trying to read it directly from the state, instead of from a reducer in the state.
Edited: I missed another point. Your reducer always has to return the whole state instead of just an attribute of it.
import { constants } from '../_constants';
const initialState = {items: [], loading: false, selected: null, error: null}
export function competitions(state = initialState, action) {
switch (action.type) {
case constants.GETALL_REQUEST:
/*return {
loading: true
};*/
//returning that I will overwrite your competition state with this object.
// this will keep all the competition state and will gerenate a new object changing only the loading attribute
return {
...state,
loading:true
}
case constants.GETALL_SUCCESS:
console.log("the action value: ", action)
return {
...state,
items: action.competitions
};
case constants.GETALL_FAILURE:
console.log("the failed action value: ", action)
return {
...state,
error: action.error
};
default:
return state
}
}

Related

Can't send data to the state

I have been trying to add the functionality of adding favourites in the movie app I created by watching a tutorial about Redux, but for some reason the state is always empty.
This is what my action and reducers look like
Reducers
const initState = {
favourites: [],
};
const favouriteReducer = (state = initState, action) => {
switch (action.type) {
case "ADD_FAVOURITE":
return {
...state,
favourites: action.payload.favourites,
};
default:
return { ...state };
}
};
export default favouriteReducer;
actions
export const favouriteActions = () => {
return {
type: "ADD_FAVOURITE",
payload: "zz",
};
};
The action is dispatched and showed in the redux dev tools too but nothing is added to the favorite state which I have created.
I have this onclick event set to an image of the star on which I actually want to pass in the Id of the movie which I have access to from another state.
const addFav = () => {
dispatch(favouriteActions(id));
};
reutrn{
<img src={favNot} onClick={addFav} />
}
I am just posting the main part of my component file here. I have also attached an Image showing my current state after I click in the image.
return {
...state,
favourites: action.payload.favourites,
};
Just only need "action.payload" or if you want favourites like array should change reducer : favourites : [...state.favourites,action.payload]
You are using
case "ADD_FAVOURITE":
in reducer but in action the type is
type: "ADD_FAVOURITES",
Could this be the cause?
export const favouriteActions = () => {
return {
type: "ADD_FAVOURITE",
favourites: "zz",
};
};
Please check whether above works. Instead of "playload" change to "favourites"

Why this redux call is giving me error when refresh the page?

I am new to redux,
i am trying to learn how to use redux right now
what i am doing now is i am trying to get data from the redux store
here is the syntax
class Posts extends Component {
componentDidMount() {
this.props.fetchPosts();
}
render() {
console.log(this.props.posts);
let postItems = this.props.posts.map((post) => (
<div key={post.id}>
<h3>{post.title}</h3>
<p>{post.body}</p>
</div>
));
return <div>{postItems}</div>;
}
}
const mapStateToProps = (state) => ({
// coming from root reducer
posts: state.posts.items,
});
export default connect(mapStateToProps, { fetchPosts })(Posts);
As you can see in the code the props items are coming from the redux store via mapStatetoProp
The problem is like this
When I change some of the code and save the code the posts are loading fine and were displayed on the page.
But When I refresh the page ctrl r the page will giving me this following error.
TypeError: this.props.posts is undefined
This always happen whenever i tried to refresh the page. I don't understand why this is happening.
here is the post reducer
import { FETCH_POSTS, NEW_POST } from "../actions/types";
const inititalState = {
// posts that comein from actions
itemsArray: [],
// object that we post
item: {},
};
export default function (state = inititalState, action) {
switch (action.type) {
case FETCH_POSTS:
// console.log('reducer fetching')
return {
...state,
items: action.payload,
};
default:
return state;
}
}
In your initialState you don't have any value set for items:
const inititalState = {
// posts that comein from actions
itemsArray: [],
// object that we post
item: {},
};
items is the property used in your component:
posts: state.posts.items
and it is only by your reducer when actions FETCH_POSTS is dispatched.
This is why you get the undefined error on page refresh/load, that property (items) just does not exist in your state at startup.
Just define items as an empty array to fix the issue:
const inititalState = {
// posts that comein from actions
itemsArray: [],
// object that we post
item: {},
items: []
};
Can you try let postItems =this.props.posts && this.props.posts.map((post) => //rest of the code

Enable one component do display data from two api fetches

For a react application, I'm using Redux to fetch data from an API. In this application, there exists a component that is displayed twice on the same page. The component is connected to an action and reducer.
Both instances of the component should display different data: one displays someone's job and the other displays someone's phone number. Both of these things are requested in separate API calls, causing the problem that the data of the second call, overwrites the data obtained in the first call in the reducer connected to the component. How would it be possible to make two API calls for such a component that is shown twice, such that both instances of it show the data of either one of these api calls?
I tried the following: make one request and fill the reducer. Then make the other request and merge the results of both in the reducer. However, the problem with this approach is that it is also possible to display only one of the components in the application.
This is the react component:
class Display extends React.Component {
componentWillMount() {
const { fetchpayload } = this.props;
fetchpayload(this.props.parameter);
}
render() {
return (
<h1>{this.props.payload}</h1>
);
}
}
const mapStateToProps = (state) => ({
payload: state.DisplayReducer.payload,
});
const mapDispatchToProps = (dispatch) => bindActionCreators(
{
fetchpayload: payloadAction,
},
dispatch,
);
This is the reducer
const initialState = {
payload: [],
};
export function DisplayReducer(state = initialState, action) {
switch (action.type) {
case 'FETCHED_DISPLAY':
return {
...state,
payload: action.payload,
};
return state;
}
}
The action file makes the request, and dispatches 'FETCHED_DISPLAY'.
just don't override your stuff inside reducer
const InitialState = {
person: {
name: '',
job: ''
}
};
export default (state = InitialState, action) => {
switch(action.type) {
case NAME:
return {
...state.person,
person: {
job: ...state.person.job,
name: action.payload
}
}
case JOB:
return {
...state.person,
person: {
name: ...state.person.name,
job: action.payload
}
}
// do that with whatever property of object person you would want to update/fetch
}
}
}
Hope this helps.

Props not updating when redux state change in React Hooks

I am trying to implement Redux on a React Hooks project, but it doesnt seems to work good. Am I doing something wrong here?
reducer.js
const initialState = {
educations: []
};
export default function home(state = initialState, action){
switch(action.type){
case GET_EDUCATIONS: {
state.educations = action.payload;
return state;
}
default:
return state;
}
}
action.js
import * as types from '../constans/home';
export const getEducations = () => {
return dispatch => {
const edus = [
{value: 1, name: 'Bachelor'},
{value: 2, name: "Master"}
]
dispatch({
type: types.GET_EDUCATIONS,
payload: edus
})
}
}
component
import React, {useEffect} from 'react';
import {connect} from 'react-redux';
import {getEducations} from '../../redux/actions/home';
function Header({educations, getEducations}) {
useEffect(() => {
getEducations(); //calling getEducations()
}, [])
useEffect(() => {
console.log(educations) //console educations after every change
})
return (
<div className="main-header">
</div>
)
}
const mapStateToProps = (state) => {
return {
educations: state.home.educations
}
}
const mapDispatchToProps = (dispatch) => {
return {
getEducations: () => { dispatch(getEducations())}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Header);
And the education property in Header function is always an empty array, as in initialState.
While when I check on browser with Redux Devtools, it shows that the state contains those two object in array.
So no matter if I change the redux state or not, the properties of the component are going to stay as initialState.
In redux, you should avoid directly mutating the state of your reducer. Refrain from doing something like state.reducers = blah. In order for redux to know that you are trying to make an update to state, you need to return a completely new state object. Following these principles, your reducers will update correctly and your components will get the new data.
Reducer.js
const initialState = {
educations: []
};
export default function home(state = initialState, action){
switch(action.type){
case GET_EDUCATIONS: {
return {
...state,
educations: action.payload
};
}
default:
return state;
}
}
In the code above, we return a new state object. It will include everything from the existing state, hence ...state, and we just update the educations property with the action.payload.
Can try with the reducer written this way :
const initialState = {
educations: []
};
export default function home(state = initialState, action){
switch(action.type){
case GET_EDUCATIONS:
return {
...state, educations:action.payload
}
default:
return state;
}
}
It looks like you’re mutating the state in the reducer. The reducer should always return a new state object if something updated.
You could do what the answers above suggest, but i would recommend using a package like immer (https://www.npmjs.com/package/immer) or immutable.js to prevent any bugs down the line. Using the spread syntax can be dangerous if your state object has some deeply nested properties, and it’s hard to be 100% sure that you haven’t accidentally mutated something, especially as your app grows in size.
It looks like you have solved this while I was getting this typed up - I decided to post it regardless, as it may be helpful.
On top of what Christopher Ngo already mentioned, the following example outlines how you can interact with your store to create new educations and then view them, in separate components..
Cheers!
I encounter this all the time and resolved it with CLEAR then GET/SET state. This ensures a reset of the state call.
Reducers.js
const initialState = {
educations: []
};
export default function home(state = initialState, action){
switch(action.type){
case GET_EDUCATIONS: {
return {
...state,
educations: action.payload
};
}
case CLEAR_EDUCATIONS: {
return initialState;
}
default:
return state;
}
}
Hooks.js
...
const clearEducation = () => {
dispatch({ type: CLEAR_EDUCATION });
}
const getEducations = (payload) => {
clearEducation(); // this clearing of the state is key fire re-render
dispatch({ type: GET_EDUCATIONS, payload });
};
}

Independent saga call/responses across different instances of same component?

I'm a little new to react, redux, and sagas, but I'm getting the hang of things.
I have a component (Results.jsx) that displays results of a particular real-world event, through a saga calling an external API:
componentDidMount() {
if (this.props.thing_id) {
this.props.getResults(this.props.thing_id);
}
}
...
const mapStateToProps = (state) => {
return {
prop1: state.apiReducer.thing_results.data1,
prop2: state.apiReducer.thing_results.data2,
fetching: state.apiReducer.fetching,
error: state.apiReducer.error,
};
};
const mapDispatchToProps = (dispatch) => {
return {
getResults: (thing_id) => dispatch({type: "RESULTS_DATA_REFRESH", payload: thing_id})
};
};
This all works great. Until... Well, I'm using a tabbed interface that lets me dynamically add a bunch of additional instances of Results.jsx so I can see a bunch of different results sets all on the same screen.
The problem is that when a new instance of the Results.jsx component loads, and gets data from the RESULTS_DATA_REFRESH dispatch, all of the instances of the Results.jsx component update with the data that comes back. They all show the same data.
For the life of me, I can't figure out how to have a particular instance of a component only listen to results from what it itself dispatched. I thought that's the way sagas were supposed to work?
Any help is appreciated!
Edits/Answers:
Reducer function is pretty textbook, looks like:
const initialState = {
fetching: false,
error: null,
thing_results: {
data1: null,
data2: null,
},
};
export default (state = initialState, action) => {
switch (action.type) {
//...
case "RESULTS_DATA_REFRESH":
return {...state, fetching: true};
case "RESULTS_DATA_SUCCESS":
return {...state, fetching: false, thing_results: action.results.data, error: null};
case "RESULTS_DATA_FAILURE":
return {...state, fetching: false, thing_results: null, error: action.error};
default:
return state;
}
};
Sagas are nothing but a middleware to offload your async tasks and store writes out of the View layer. Ultimately the prop that comes to your component depends on how you store it. Specifically in this case if prop1 and prop2 are picked up from the same place in the store, it'll come as the same value in all instances of Results.
If you require different data for different instances, section it based on some unique id mapped to the instance. You reducer would look like :
const apiReducer = (state = {}, action) => {
switch (action.type) {
case "RESULTS_DATA_REFRESH":
return {
...state,
[action.payload]: { data: null, fetching: true }
};
case "RESULTS_DATA_SUCCESS":
return {
...state,
/** You should be getting back the id from the api response.
* Else append the id in the success action from your api saga.
*/
[action.payload.id]: { data: action.results.data, fetching: false }
};
case "RESULTS_DATA_FAILURE":
return {
...state,
[action.payload.id]: {
data: null,
fetching: false,
error: action.error
}
};
default:
return state;
}
};
/** Other reducers */
const otherReducerA = function() {};
const otherReducerB = function() {};
export default combineReducers({ apiReducer, otherReducerA, otherReducerB });
And access it like :
const mapStateToProps = state => {
return {
data: state.apiReducer
};
};
function Results({ data, thing_id }) {
return <div>{data[thing_id].data}</div>;
}

Resources