Okay, so we have just started using Redux and Sagas and are using it to fetch some async data (a JsonSchema) from the server.
After fetching, the state tree looks like this:
{
"forms": {
"form-url": {
"isLoading": false,
"schema": { ... }
}
}
}
The reducer looks roughly like this:
const reducer = (state = {}, action) => {
switch (action.type) {
case FETCH:
const form = {};
form[action.url] = {
isFetching: true,
schema: {},
};
return Object.assign({}, state, form);
case FETCH_SUCCESS:
const sform = {};
sform[action.url] = {
isFetching: false,
schema: action.schema,
};
return Object.assign({}, state, sform);
}
return state;
}
Now, when mapping state to props, and the data hasn't been fetched yet, we need a ternary to make it work:
const mapStateToProps = (state, ownProps) => {
return {
schema: state.forms[ownProps.sourceUrl] ? state.forms[ownProps.sourceUrl].schema : {}
}
}
Even though this works, my spidey sense is telling me that this is a code smell. Is there a suggested pattern to solve the missing initial state?
Yes you can use reselect to do that, it's cleaner and re usable
Related
I tried lot of things but I can't achieved what want. I have an initialState in redux. It has 4 coin name as you see below. I'm trying to fill this keys with value from API. But at the end I have only 1 key with data.
import { SET_LIST, GET_LIST } from '../actions'
const initialState = {
coins: {
'BTC': {},
'ETH': {},
'LTC': {},
'DOGE': {},
},
loading: false,
error: null
}
const coinsReducers = (state = initialState, action) => {
switch (action.type) {
case SET_LIST: {
const key = action.payload.key;
const list = action.payload.list;
let obj = Object.assign({}, state);
obj.coins[key] = list;
obj.loading = true;
return obj;
}
default: return state;
}
}
export default coinsReducers
I iterate this initial state in app.js componentDidMount hook and make api call with key. When I make api call with BTC key, I want to push the response into BTC key.
I hope someone help me.
EDIT: Working Example
case SET_LIST: {
const {key, list} = action.payload;
return { ...state, loading: true, coins: {...state.coins, [key]: list }};
}
case GET_LIST:
// don't sure what you try to achieve here, anyhow what you are saying is that
// from now on all the state of the app it just state.coins
return state.coins
default: return state;
The reducer is used to update the state, not to get data from it.
The GET_LIST action returns state.coins which corrupts the state object, thus returning wrong values
Use store.getState() to get data from store
I have two dashboards which contains similar data and properties that I want to implement in redux. See below.
Dashboard 1 : {filters, widgets, custom}
Dashboard 2: {filters, widgets, custom}
I want to create my redux state like so:
{
dashboards: {
dashboard1:{
filter:{ // filter related stuff},
widgets:{ // widget related state},
custom:{ // custom state}
},
dashboard2: {
filter:{ // filter related state},
widgets:{ // widget related state},
custom:{ // custom state}
}
},
auth: {// auth related stuff},
...// other state keys
}
In order to achieve this I am trying to use combine reducers like this for dashboards
// xyz.reducer.js
combineReducers({
filters: filterReducers,
widgets: widgetReducers,
custom: CustomReducers
})
now I have created a dashboard reducer, which is called every time user navigates to a certain dashboard. I want to assign this state based on dashboardID. something like -
const dashboardReducer = (state = defaultState, action) => {
switch (action.type) {
case CREATE_DASHBOARD: {
const { payload } = action;
return {
...state,
[payload.dasboardId]: // all the combined reducer state here
};
}
default:
return state;
}
};
Can someone suggest me how can I achieve this? I am also open for suggestion if someone have a better
way of maintaing this kind of state structure.
Sure, you can do this as long as you have a way to identify all actions that are relevant to the xyz reducer and have away to identify the dashboardId.
For your example, assuming the nested reducer from xyz.reducer.js is called xyz:
import xyz from './xyz.reducer.js'; // or whatever
const dashboardReducer = (state = defaultState, action) => {
switch (action.type) {
case CREATE_DASHBOARD: {
const { payload } = action;
return {
...state,
[payload.dashboardId]: xyz(state[payload.dashboardId], action),
};
}
default:
return state;
}
};
Let's say you know all of the action types that the dashboard might need to respond to you and they all have dashboardId as a key in the payload you can simply pass all of those along.
const dashboardReducer = (state = defaultState, action) => {
switch (action.type) {
case CREATE_DASHBOARD:
case UPDATE_DASHBOARD_FILTER:
case ADD_DASHBOARD_WIDGET:
//... etc
{
const { payload } = action;
return {
...state,
[payload.dashboardId]: xyz(state[payload.dashboardId], action),
};
}
default:
return state;
}
};
This is a little brittle so you may want to do something like assume that any payload with a dashboard id should be passed on:
const dashboardReducer = (state = defaultState, action) => {
const {payload = {}} = action;
if (typeof payload.dashboardId !== 'undefined') {
return {
...state,
[payload.dashboardId]: xyz(state[payload.dashboardId], action),
};
}
return state;
};
Now you don't need to keep up the list of action types, you just need to ensure that all relevant action creators include a dashboardId in their payload.
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>;
}
I have seen that lot of tutorials were using this method in their mapStateToProps method.
(state.ReducerName)
const mapStateToProps = (state) => {
return {
invoices: state.IcmWebReducer,
}
};
But this didn't work for me. What was working for me was this
(state.objectParameterName)
const mapStateToProps = (state) => {
return {
params: state.params,
invoices: state.invoices
}
};
My reducer as below
const initialState = {
invoices : [],
params: {
status: 'Pending',
_sort: 'documentInfo.dueDate',
_order: 'desc',
q: ''
}
};
const IcmWebReducer = (state = initialState, action) =>{
switch (action.type){
case 'UPDATE_INVOICES':
return Object.assign({}, state, {
invoices: action.invoices
});
case 'UPDATE_PARAMS':
return Object.assign({}, state, {
params: action.params
});
default:
return state;
}
};
export default IcmWebReducer;
What is the correct method ? What is the different between this ?
(state.objectParameterName) is the correct method.
mapStateToProps is for using the redux state as props in the connected component. The reducer is a pure function, it just takes some value and returns it. The reducer updates and returns the states based upon the action type fired.
Even if you somehow define the reducer in the redux state and try to use it, it's a wrong practice.
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
}
}