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>;
}
Related
I've been working with redux for the last couple weeks and was incorporating it into my projects when I ran into this wall. Pretty common reducer for modals being rendered so i can animate them before unmounting them.
const initialState = {
isModalOpen: false,
test: false
}
export default function(state = initialState, action) {
switch (action.type) {
case "modalInteraction":
return {
isModalOpen: action.payload
};
case "testModalInteraction":
return {
test: action.payload
};
default:
return state;
};
}
Sadly, the test property is still returning as undefined despite the fact that the other initial state in the same reducer can be called without a problem. I even removed all the testModalInteraction dispatches in the case that that somehow upset the datatype. I just can't spot the difference that keeps returning undefined.
When you return the new state, make sure to spread the initial state (...state) and then change whatever values you need to change.
const initialState = {
isModalOpen: false,
test: false
}
export default function(state = initialState, action) {
switch (action.type) {
case "modalInteraction":
return {
...state,
isModalOpen: action.payload
};
case "testModalInteraction":
return {
...state,
test: action.payload
};
default:
return state;
};
}
If it is still undefined, make sure the payloads are defined for both actions.
For example, your modalInteraction action could look like
export const modalInteraction = (bool) => ({
type: "modalInteraction",
payload: bool
})
P.S., you can destructure the action object. This allows you to use "type" instead of "action.type" and "payload" instead of "action.payload".
const initialState = {
isModalOpen: false,
test: false
}
export default function(state = initialState, action) {
const {type, payload} = action;
switch (type) {
case "modalInteraction":
return {
...state,
isModalOpen: payload
};
case "testModalInteraction":
return {
...state,
test: payload
};
default:
return state;
};
}
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
}
}
I'm trying to delete an element from dom by clicking on it. I did it without the problem without redux thunk but now I have a problem. My reducer doesn't know about the state. How do let him know what items are?
Action:
export function deleteItem(index) {
return {
type: 'DELETE_ITEM',
index
};
}
My reducer that shows undefined.
export function deleteItem(state = [], action) {
switch (action.type) {
case 'DELETE_ITEM':
const copy = state.items.slice()
console.log(copy)
default:
return state;
}
}
Heres my actual code https://github.com/KamilStaszewski/flashcards/tree/develop/src
I saw your code and you are defining a new reducer for each of the operations you want to get done to your items (e.i itemsHaveError, deleteItem, ...) but the correct way of doing this is to store all of the relevant functions for the items to a single reducer which holds the data needed to change whenever some action to the items happens, but in the way you did it, any time any action happens because your reducers are separated the initial state gets empty as you have passed to the functions and the reducers do not know about their related data so they overwrite them with the empty initial state, the correct way would be like this to write a single reducer for items:
const initialState = {
isLoading: false,
hasError: false,
items: [],
};
export default function(state = initialState, action) {
switch (action.type) {
case ITEMS_HAVE_ERROR:
return {
...state,
hasError: action.hasError,
};
case ITEMS_ARE_LOADING:
return {
...state,
isLoading: action.isLoading,
};
case ITEMS_FETCH_DATA_SUCCESS:
return {
...state,
items: action.items,
};
case DELETE_ITEM:
const copy = state.items.slice()
return {
...state,
items: copy,
};
default:
return state;
}
}
so this would be your item.js and your item reducer and the only one that should get to combineReducer function.
Indicate the initial State of the reducer by default , the state is an empty array and you can't access the state.items , cause it is undefined. Assume this:
const x = [];
x.foo.slice();
that would return an error . Thus from :
state = []
change it to :
state = {
items:[]
}
applying it to your code:
export function deleteItem(
state = {
items:[]
},
action) {
switch (action.type) {
case 'DELETE_ITEM':
const copy = state.items.slice()
console.log(copy)
default:
return state;
}
}
I am new in react and redux and I would like to know if it is possible to use one store for multiple actions.
I am trying this but the first action ges overwritten with the last action, why?
I am calling the action in two separate component and I call those two component in my app component.
reducer.js
const dataReducer = (state = {
fetching: false,
fetched: false,
data: {},
error: null
}, action) => {
switch (action.type) {
case 'FETCH_DATA_PENDING':
return {...state, fetching: true}
break;
case 'FETCH_DATA_FULFILLED':
return {...state, fetching: false, fetched: true, data: action.payload.data }
break;
case 'FETCH_DATA_REJECTED':
return {...state, fetching: false, error: action.payload }
break;
}
return state;
}
module.exports = dataReducer;
action.js
import axios from 'axios';
const apiUrl = 'https://swapi.co/api/';
//fetch categories
export function fetchCategories() {
return {
type: 'FETCH_DATA',
payload: axios.get(apiUrl)
}
}
//fetch films
export function fetchFilms() {
return {
type: 'FETCH_DATA',
payload: axios.get(apiUrl + 'films')
}
}
You should be able to do it this way, but the fact that both of your actions have the same type might be confusing in your reducers. It might be more helpful to have a type FETCH_FILMS and FETCH_CATEGORIES. That way the reducer can do separate things with them, unless of course, you always want every reducer to do the exact same thing with them.
I'm quite new to Redux and from what I understand, a reducer should be created for each type of object. E.g. for user interaction a user reducer should be created. My question is: How do you handle cases where you require the object for different purposes?
Scenario: Imagine having a user reducer which returns the current user. This user would be required in the entire application and needed for general controls on every page.
Now what happens when you need to load another user which is used for different purposes. E.g. profile page: loading a user to display information.
In this case there would be a conflict if the user reducer would be used. What would be the correct way to handle this in Redux? In case a different reducer would have to be created, what would be the naming convention for the new reducer?
First, you've mentioned:
a user reducer which loads the current user
I don't know if I got you correctly, but if this means you want to fetch (from an API, for example) the current user inside the reducer, this is a wrong approach.
Reducers are intended to be pure functions. You can call them with the same arguments multiple times and they will always return the same expected state.
Side effects like that should be handled by action creators, for example:
actions/user.js
export const FETCH_ME = 'FETCH_ME'
export const FETCH_ME_SUCCESS = 'FETCH_ME_SUCCESS'
// it's using redux-thunk (withExtraArgument: api) module to make an async action creator
export const fetchMe = () => (dispatch, getState, api) => {
dispatch({ type: FETCH_ME })
return api.get('/users/me').then(({ data }) => {
dispatch({ type: FETCH_ME_SUCCESS, data })
return data
})
}
Inside your reducer you can simple get the data and set a new state (note that if you send the action with the same data multiple times, the state will always be the same).
reducers/user.js
import { FETCH_ME, FETCH_ME_SUCCESS } from '../actions/user'
const initialState = {
item: null,
loading: false
}
export const userReducer = (state = initialState, action) => {
switch (action.type) {
case FETCH_ME:
return {
...state,
loading: true
}
case FETCH_ME_SUCCESS:
return {
...state,
loading: false,
item: action.data
}
default:
return state
}
}
Now, for your scenario:
Now what happens when you need to load another user which is used for different purposes. E.g. profile page: loading a user to display information.
You will just write another action creator for that:
actions/user.js
export const FETCH_ME = 'FETCH_ME'
export const FETCH_ME_SUCCESS = 'FETCH_ME_SUCCESS'
export const FETCH_USER = 'FETCH_USER'
export const FETCH_USER_SUCCESS = 'FETCH_USER_SUCCESS'
export const fetchMe = () => (dispatch, getState, api) => {
dispatch({ type: FETCH_ME })
return api.get('/users/me').then(({ data }) => {
dispatch({ type: FETCH_ME_SUCCESS, data })
return data
})
}
export const fetchUser = (id) => (dispatch, getState, api) => {
dispatch({ type: FETCH_USER })
return api.get(`/users/${id}`).then(({ data }) => {
dispatch({ type: FETCH_USER_SUCCESS, data })
return data
})
}
Then you adapt your reducer to manage more sets:
reducers/user.js
import { combineReducers } from 'redux'
import { FETCH_ME, FETCH_ME_SUCCESS, FETCH_USER, FETCH_USER_SUCCESS } from '../actions/user'
const initialState = {
item: null,
loading: false
}
const meReducer = (state = initialState, action) => {
switch (action.type) {
case FETCH_ME:
case FETCH_ME_SUCCESS:
return userReducer(state, action)
default:
return state
}
}
const activeReducer = (state = initialState, action) => {
switch (action.type) {
case FETCH_USER:
case FETCH_USER_SUCCESS:
return userReducer(state, action)
default:
return state
}
}
const userReducer = (state = initialState, action) => {
switch (action.type) {
case FETCH_USER:
case FETCH_ME:
return {
...state,
loading: true
}
case FETCH_USER_SUCCESS:
case FETCH_ME_SUCCESS:
return {
...state,
loading: false,
item: action.data
}
default:
return state
}
}
export default combineReducers({
activeUser: activeReducer,
me: meReducer
})
Your final user state should be something like:
{
me: {
item: null,
loading: false
},
active: {
item: null,
loading: false
}
}