How to split reducer under 1 key? - reactjs

I want to separate a reducer into N more combining together 1 key.
Say I have an initial state:
const STATE_INITIAL = {
nested_component: {
field1: 1,
field2: 2
},
upper_field: 3
}
Then I have a reducer:
function reducer(state=STATE_INITIAL, action){
switch(action){
case ACTION_UPPER_FIELD:
return ...
case ACTION_GRID1:
return ...
case ACTION_GRID2:
return ...
default:
return state;
}
}
Why I want to do it?
I want to have a component that I can reuse throughout the project. It would always come with its initial state and would have its reducer that I'd like to connect to the rest of the application.
My solution
One way I can think of is stacking cases for grid actions, providing it with state.gridand its own initial state and combing the result with the state:
const STATE_INITIAL = {
nested_component: {},
upper_field: 3
};
function reducer(state=STATE_INITIAL, action){
switch(action){
case ACTION_UPPER_FIELD:
return ...
case ACTION_GRID1:
case ACTION_GRID2:
return reducerGrid(state.grid, action);
default:
return state;
}
}
const STATE_INITIAL_GRID = {
field1: 1,
field2: 2
};
function reducerGrid(state = STATE_INITIAL_GRID, action) {
switch(action){
case ACTION_GRID1:
return ...
case ACTION_GRID2:
return ...
default:
return state;
}
}
Is there a standardized approach or is my solution fine? The things I don't like about it is the default in the reducerGrid seems redundant now and I am also not satisfied with having to repeat the actions in both reducers.
My 2nd Solution
function reducer(state=STATE_INITIAL, action){
const stateGrid = reducerGrid(state.grid, action)
let stateNew = state;
if(stateGrid !== state.grid){
stateNew = {...state, grid: ...stateGrid}
}
switch(action){
case ACTION_UPPER_FIELD:
return {...stateNew, ... };
default:
return stateNew;
}
}
3rd Solution
function reducer(state=STATE_INITIAL, action){
const stateNew = {...state, grid: ...reducerGrid(state.grid, action)};
switch(action){
case ACTION_UPPER_FIELD:
return ...
default:
return stateNew;
}
}

I have finally found a solution I am satisfied with.
Using this method:
import R from 'ramda';
function splitReducers(reducers, rest) {
return (state, action) => {
const reducersPrepared = R.mapObjIndexed((reducer, key) => {
return reducer(R.defaultTo({}, state)[key], action);
})(reducers);
const getUndefinedIfEmpty = R.ifElse(
R.isEmpty,
() => undefined,
R.identity
);
const stateWithoutSplitKeys = R.pipe(
R.omit(R.keys(reducers)),
getUndefinedIfEmpty
)(state);
return R.merge(
reducersPrepared,
rest(stateWithoutSplitKeys, action)
);
}
}
I can write my state tree in the following way:
Ports: splitReducers({
grid: reducerGrid,
}, reducer);
This will result in the object with keys split:
{
Ports: {
grid: {...},
isSaving: true,
isVisible: false
}
}
After applying the method the root-reducer is showing more of its state at the first glance:
export const rootReducer = combineReducers({
pageAllocation: combineReducers({
tabNetwork: combineReducers({
popupNetworkTemplates: reducerPopupNetworkTemplates,
gridPorts: splitReducers({ // <----- HERE IT IS
grid: reducerGridPortsOnly
}, reducerPorts),
}),
tabStorage: () => ({}),
activeTab: reducerPortsActiveTab
}),

Related

how to save array object data in redux store

i try to store multiple object in redux store on my react native app, but only one object is save,
i'm new at redux, i try a lot of solutions found on StackOverflow but no one works :/
result i have in my store:
"hives": {"hive_id": 12944, "hive_name": null}
result i want (or something like that) :
"hives": [
1: {"hive_id": 123, "hive_name": "HelloHive"},
2: {"hive_id": 12944, "hive_name": null}]
store:
const middleware = [thunk]
export const store = createStore(persistedReducer, applyMiddleware(...middleware));
export const persistor = persistStore(store);
reducer :
const INIT_STATE = {
hives: [],
}
const hiveReducer = (state = INIT_STATE, action) => {
switch (action.type) {
case SET_HIVES:
return {
...state,
hives: action.payload,
};
[...]
action creator:
export const setHives = hives => {
return {
type: SET_HIVES,
payload: hives,
};
};
action:
export const getHives = () => {
return dispatch => {
axios.get(GET_HIVE_URL, HEADER).then(res => {
const status = res.data.status;
const hives = res.data.hives;
if (status == 'hiveFound') {
for (let i = 0; i < hives.length; i++) {
console.log(hives[i]);
dispatch(setHives(hives[i]));
}
}
});
};
};
and my API send me:
"hives": [
{
"hive_id": 123,
"hive_name": "HelloHive"
},
{
"hive_id": 12944,
"hive_name": null
}
]
and console.log(hives[i]) return :
LOG {"hive_id": 123, "hive_name": "HelloHive"}
LOG {"hive_id": 12944, "hive_name": null}
thanks you
First of all, in your reducer you don't need to use ...state spread operator, since hives seems to be the only one variable in your state there. And second, you are iterating over each element of hives, therefore you are inputting them one by one thus overwriting the previous one. You are not appending it to array. Here's how you need to change your action:
export const getHives = () => {
return dispatch => {
axios.get(GET_HIVE_URL, HEADER).then(res => {
const status = res.data.status;
const hives = res.data.hives;
if (status == 'hiveFound') {
dispatch(setHives(hives));
}
});
};
};
This way it will write the whole array into that variable in redux.
You can try this below so you can store the whole array. assuming you already have the actions.
InitialState
export default {
hives:[]
}
HivesReducer
export default function counter(state = initialState.hives, action) {
switch (action.type) {
case Types.SET_HIVES:
return [...state, action.payload];
default:
return state;
}
}
In your reducer try this :
case SET_HIVES:
return {
...state,
hives: [...state.hives,action.payload],
};
[...]
hope it helps. feel free for doubts

React Native Flatlist extraData not working redux data changed

I have array in redux. I am showing datas on Flatlist. However, When I edited array data , flatlist not re-render. How can i solve this problem? I checked my redux and is working fine
this.props.notes[this.state.Index]={
color: JSON.stringify(BgColor),
date: this.state.fullDate.toString(),
note: this.state.noteText,
name: this.state.noteName,
type: "note",
noteID:this.props.notes[this.state.Index].noteID
}
this.props.editNotes(this.props.notes);
Flatlist code;
<FlatList
ref={(list) => this.myFlatList = list}
data={this.props.notes}
showsVerticalScrollIndicator={false}
renderItem={({item, index})=>(
)}
removeClippedSubviews={true}
extraData={this.props.notes}
/>
mapStateToProps on same page with Flatlist
const mapStateToProps = (state) => {
const { notes } = state
return { notes }
};
Reducer
const notes = [];
const notesReducer = (state = notes, action) => {
switch (action.type) {
case 'editNotes':
return state = action.payload;
default:
return state
}
};
export default notesReducer;
The reason it's not updating is because you're not returning a new array. The reference is same.
Return the updated state like return [...state,action.payload]
The reason it's not updating the data correctly is because the mutation.
The problematic code is this part.
this.props.notes[this.state.Index]={
color: JSON.stringify(BgColor),
date: this.state.fullDate.toString(),
note: this.state.noteText,
name: this.state.noteName,
type: "note",
noteID:this.props.notes[this.state.Index].noteID
}
this.props.editNotes(this.props.notes);
It should be in this way
const { notes, editNotes } = this.props;
const newNotes = [...notes];
const { index } = this.state;
newNotes[index] = {
//update data
}
editNotes(newNotes);
You can fix the issue in many ways but the wrong part I see in your code is Reducer. As per the standard, your reducer should be a Pure Function and the state should not mutate.
const notes = [];
const notesReducer = (state = notes, action) => {
switch (action.type) {
case 'editNotes':
return {
...state,
...action.payload;
},
default:
return state
}
};
export default notesReducer;
This should resolve your issue.
Suggestion:
Try to create a nested hierarchy in redux like
const initialState = {
notes: [],
};
const notesReducer = (state = initialState, action) => {
switch (action.type) {
case 'editNotes':
return {
...state,
notes: [
...state.notes,
...action.payload.notes,
],
},
default:
return state
}
};
export default notesReducer;

Redux store doesn't update all changes in app.js

such an honor to drop my first question in this community! I'm working on a recipe app where I use Redux to manage states. I'm using async storage to store changes locally. I'm a bit stuck now because my store only applies and stores a few changes instead of the whole recipe.
This is how the data of a recipe looks like (sorry for my Dutch):
{
cardId: 2,
time: "5 minutes",
title: "Wortel-Kokossoep met Dadelroom",
category: "ontbijt",
image: require("./assets/wortel-kokossoep.jpg"),
subtitle: "Gezonde en makkelijke soep!",
caption: "Wortel-Kokossoep met Dadelroom",
description:
"Begin de dag gezond met deze smoothie dat rijk is aan vitamines.",
stepOne: "Stap 1: Voeg alles toe aan de NutriBullet of blender.",
stepTwo:
"Stap 2: Blend twee keer gedurende ongeveer 5 tot 10 seconden en je bent klaar!",
stepThree: "",
stepFour: "",
stepFive: "",
stepSix: "",
stepSeven: "",
stepEight: "",
favorite: false
},
and this is how I implemented Redux in the app.js. Please forgive me for posting the whole code. I'm still a noob, eager to learn everything about Redux and react.
const reducer = (state = initialState, action) => {
switch (action.type) {
case "FAV_RECIPIE":
//const recipie = state.recipies.find(r => (r.cardId = action.id));
const recipieIndex = state.recipies.findIndex(
r => r.cardId === action.id
);
const currentValue = state.recipies[recipieIndex].favorite;
state.recipies[recipieIndex].favorite = !currentValue;
state.recipies = [...state.recipies];
saveRecipes(state.recipies); // save to local storage
return { ...state };
case "SET_LOADED_RECIPIES":
console.warn("!!!!");
if (action.recipies) {
state.recipies = [...JSON.parse(action.recipies)]; // JSON parse to convert string back to list
}
console.log("set recipies");
return { ...state };
case "OPEN_MENU":
return { action: "openMenu" };
case "CLOSE_MENU":
return { action: "closeMenu" };
default:
return state;
}
};
const saveRecipes = async recipies => {
try {
await AsyncStorage.setItem("#VV:Recipes", JSON.stringify(recipies)); // JSON stringify to convert list to string (for storage)
} catch (error) {
// error saving, and that is fine =)
console.log("could not save recipes");
}
};
const store = createStore(reducer, initialState);
store.subscribe(() => {
console.log("store changed", store.getState().recipies);
});
const App = () => (
<Provider store={store}>
<AppNavigator />
</Provider>
);
export default App;
I really hope some of you can help me out! Thanks in advance!
There's a couple of things going wrong in your reducer, but the big thing is doing state-mutations. You want to avoid logic like:
state.recipies[recipieIndex].favorite = !currentValue;
also
state.recipies = [...state.recipies];
This is against redux principles. You never want to directly change values of the state without first making a copy or clone.
So we will go with creating a shallow-copy of state in your reducer and make updates to that instead:
const reducer = (state = initialState, action) => {
switch (action.type) {
case "FAV_RECIPIE":
var newState = {...state}
//const recipie = state.recipies.find(r => (r.cardId = action.id));
const recipieIndex = state.recipies.findIndex(
r => r.cardId === action.id
);
const currentValue = state.recipies[recipieIndex].favorite;
newState.recipies[recipieIndex].favorite = !currentValue;
saveRecipes(newState.recipies); // save to local storage
return { ...newState };
case "SET_LOADED_RECIPIES":
console.warn("!!!!");
var newState = [...state]
if (action.recipies) {
newState.recipies = [...JSON.parse(action.recipies)]; // JSON parse to convert string back to list
}
console.log("set recipies");
return { ...newState };
case "OPEN_MENU":
return { action: "openMenu" };
case "CLOSE_MENU":
return { action: "closeMenu" };
default:
return state;
}
};
Alternatively we can handle this succinctly using .map() which creates a copy for us.
const reducer = (state = initialState, action) => {
switch (action.type) {
case "FAV_RECIPIE":
const updatedRecipes = {
...state,
recipes: state.recipes.map(recipe => {
if (recipe.cardId === action.id) {
return {
...recipe,
favorite: !recipe.favorite
};
} else {
return recipe;
}
})
};
saveRecipes(updatedRecipes)
return {
...updatedRecipes
}
case "SET_LOADED_RECIPIES":
var newState = {...state};
if (action.recipies) {
newState.recipies = [...JSON.parse(action.recipies)]; // JSON parse to convert string back to list
}
return { ...newState };
case "OPEN_MENU":
return { action: "openMenu" };
case "CLOSE_MENU":
return { action: "closeMenu" };
default:
return state;
}
};

Redux overwrites model with previous state

I am currently making a sample project in AngularJs combined with Redux.
I am struggling to get the mappings from the reducer working.
I have a simple input where users can set a new name together with a drop down to select a 'company'.
<input type="text" ng-model="$ctrl.single.object.name">
<select ng-change="$ctrl.getProperties()"
ng-options="option.description as option.description for option in $ctrl.list.all"
ng-model="$ctrl.single.object.company">
When the user changes the company, new properties need to be fetched in order for the user to set these properties.
function FooController($ngRedux, FooActions, BarActions) {
this.$onInit = function () {
this.unsubscribeCompanies = $ngRedux.connect(this.mapStateToThis, BarActions)(this);
this.fetchCompanyList();
};
this.$onDestroy = function () {
this.unsubscribeCompanies();
};
this.fetchCompanyList = function () {
this.fetchCompanies().payload.then((response) => {
this.fetchCompaniesSuccess(response.data);
}, (error) => {
this.fetchCompaniesError(error.data);
});
};
this.getProperties = function () {
this.fetchCompanyProperties(this.single.object.company).payload.then((response) => {
this.fetchCompanyPropertiesSuccess(response.data);
}, (error) => {
this.fetchCompanyPropertiesError(error.data);
});
};
this.mapStateToThis = function (state) {
return {
list: state.bar.list,
single: state.bar.single
};
};
}
module.exports = {
template: require('./index.html'),
controller: ['$ngRedux', 'FooActions', 'BarActions', FooController]
}
The problem I get is that the name and the selected company are overwritten with empty values when the fetch for properties is successful. I get why the values are overwritten with empty values and I have found a way to get it working.
export const GET_COMPANIES = 'GET_COMPANIES';
export const GET_COMPANIES_SUCCESS = 'GET_COMPANIES_SUCCESS';
export const GET_COMPANIES_ERROR = 'GET_COMPANIES_ERROR';
export const GET_COMPANIES_PROPERTIES = 'GET_COMPANIES_PROPERTIES';
export const GET_COMPANIES_PROPERTIES_SUCCESS = 'GET_COMPANIES_PROPERTIES_SUCCESS';
export const GET_COMPANIES_PROPERTIES_ERROR = 'GET_COMPANIES_PROPERTIES_ERROR';
export default function BarActions($http) {
function fetchCompanies() {
return {
type: GET_COMPANIES,
payload: $http.get('api/companies')
};
}
function fetchCompaniesSuccess(companies) {
return {
type: GET_COMPANIES_SUCCESS,
payload: companies
};
}
function fetchCompaniesError(error) {
return {
type: GET_COMPANIES_ERROR,
payload: error
};
}
function fetchCompanyProperties(company) {
return {
type: GET_COMPANIES_PROPERTIES,
payload: $http.get(`api/company/${company}/properties`)
};
}
function fetchCompanyPropertiesSuccess(properties) {
return {
type: GET_COMPANIES_PROPERTIES_SUCCESS,
payload: properties
};
}
function fetchCompanyPropertiesError(error) {
return {
type: GET_COMPANIES_PROPERTIES_ERROR,
payload: error
};
}
return {
fetchCompanies,
fetchCompaniesSuccess,
fetchCompaniesError,
fetchCompanyProperties,
fetchCompanyPropertiesSuccess,
fetchCompanyPropertiesError
}
}
The way I overwrite the values in the reducer is as follows:
import { GET_COMPANIES, GET_COMPANIES_SUCCESS, GET_COMPANIES_ERROR, GET_COMPANIES_PROPERTIES, GET_COMPANIES_PROPERTIES_ERROR, GET_COMPANIES_PROPERTIES_SUCCESS } from "../actions/bar.actions";
const all = [];
const initialState = {
list: {
all,
filtered: all,
error: null,
loading: false
},
single: {
object: {},
error: null,
loading: false
}
};
export function BarReducer(state = initialState, action) {
switch (action.type) {
case GET_COMPANIES:
return { ...state, list: { all: [], filtered: [], error: null, loading: true } };
case GET_COMPANIES_SUCCESS:
return { ...state, list: { all: action.payload, filtered: action.payload, error: null, loading: false } };
case GET_COMPANIES_ERROR:
return { ...state, list: { all: [], filtered: [], error: action.payload.innerException, loading: false } };
case GET_COMPANIES_PROPERTIES:
return { ...state, single: { ...state.single, object: { ...state.single.object }, error: null, loading: true } };
case GET_COMPANIES_PROPERTIES_SUCCESS:
return { ...state, single: { ...state.single, object: { ...state.single.object, payloadValues: action.payload }, error: null, loading: false } };
case GET_COMPANIES_PROPERTIES_ERROR:
return { ...state, single: { object: null, error: action.payload.innerException, loading: false } };
default:
return state;
}
}
The way I now use the spread operator in order to overwrite the old state feels dirty. I was wondering if there are any rules or guidelines to handle this issue. So far I have searched a while on internet and in specific the Redux website but I did not come cross any other solutions.
The breakage is likely due to the structure of the reducer. It is concerned with too many different parts of state and has to operate on deep nested objects, making it easy to accidentally mutate state. The guidelines for reducer structure say that splitting reducer state into normalized slices is the best way to go.
Try splitting your one reducer into multiple smaller reducers. For example:
export const all = (initialAll = [], { type, companies }) => {
switch(type) {
case GET_COMPANIES_SUCCESS: return companies;
default: return initialAll;
}
}
export const error = (initialError = '', { type, error }) => {
switch(type) {
case GET_COMPANIES_ERROR: return error;
default: return initialError;
}
}
export const isFetching = (isFetching = false, { type }) => {
switch(type) {
case GET_COMPANIES: return true;
case GET_COMPANIES_SUCCESS: return false;
case GET_COMPANIES_ERROR: return false;
default: return isFetching;
}
}
Then, compose them into one reducer:
import { combineReducers } from 'redux';
export list = combineReducers({
all,
error,
isFetching
});
// ...
export rootReducer = combineReducers({
list,
single,
// ...
})
This way, each reducer is concerned with only one thing or set of things, and its reduction handlers can do simple operations on single-level state instead of complex operations on deep nested state.
Also, in your list substate, it looks like you are storing the same type of collection resources in both all and filtered with potential overlap. This leads to multiple sources of truth for the same data, which opens the door to data inconsistency. Instead, keep an array of filteredIds:
export const filteredIds = (initialIds = [], { type, filteredIds }) => {
switch(type) {
case SET_FILTERED_IDS: return filteredIds;
default: return initialIds;
}
}
Then, use a selector that filters all by the filteredIds to get your filtered items.
One option is to use Immutable, which would change your reducers to:
case GET_COMPANIES:
return state.setIn(['list', 'loading'], true);
// etc
See Using Immutable.JS with Redux for more information about this approach.
Another option is to use Lodash, as shown in this Issue, you can define the following function to make it similar to the immutable one:
import {clone, setWith, curry} from 'lodash/fp';
export const setIn = curry((path, value, obj) =>
setWith(clone, path, value, clone(obj)),
);
Then you can use setIn as follow:
case GET_COMPANIES:
return setIn(['list', 'loading'], true, state);
// etc
The Lodash approach is just working with plain object, so it might be easier to understand than Immutable.

How to update two values in two different reducers at the same time in Redux / Atomic operations

Project description: I've a calendar like Google Calendar, where you have zoomIn and zoomOut buttons, and also a datepicker where you can click any date to move your calendar to that date.
Design of store: I've a filters reducer where I keep the range of the current calendar (for example {start: '01/01/2016', end: '01/01/2017'}), and also a UI reducer where I keep the current zoom level (one of year/month/week)
Scenario: When someone is on zoomLevel='year' and clicks on a date on the datepicker, I need to go to zoomLevel='week' and also modify the range.
Problem: How to update the store with both zoomLevel and range, at the same time? If I don't, my calendar breaks due to inconsistency because when I update the first value the user interface renders everything.
UI reducer as an example of generic reducer:
import * as actions from './action-types'
import { omit } from 'lodash'
import initialState from './initial-state'
export * from './actions'
export * from './selectors'
// TODO: Eval. Tendria mas sentido hacer el switch por dominio?
export default function ui(state = initialState, {type, meta: {domain = 'general'} = {}, payload: {key, value} = {}}) {
switch (type) {
case actions.SET:
return {
...state,
[domain]: {
...state[domain] || {},
[key]: value
}
}
case actions.TOGGLE:
return {
...state,
[domain]: {
...state[domain] || {},
[key]: !!!(state[domain] || {})[key]
}
}
case actions.TOGGLE_IN_ARRAY:
// No sirve para objetos
const index = state[domain] && state[domain][key] ? state[domain][key].indexOf(value) : -1
return index === -1 ?
{
...state,
[domain]: {
...state[domain] || {},
[key]: [
value,
...state[domain][key]
]
}
} : {
...state,
[domain]: {
...state[domain],
[key]: [
...state[domain][key].slice(0, index),
...state[domain][key].slice(index + 1)
]
}
}
case actions.DELETE:
return {
...state,
[domain]: omit(state[domain], key)
}
case actions.CLEAR_DOMAIN:
return {
...state,
[domain]: initialState[domain]
}
case actions.RESET_UI:
return {
...initialState
}
default:
return state
}
}
Dispatched actions will be sent to all reducers in the root reducer which will in turn be passed down to child reducers (depending on reducer implementations).
Hence if your root reducer looks like this:
export default combineReducers({
filters,
ui,
});
You can respond to the same action like this in each reducer:
function filters(state, action) {
case CLICK_ON_DATE:
return {
...state,
start: ...,
end: ...,
};
...
}
function ui(state, action) {
case CLICK_ON_DATE:
return {
...state,
zoomLevel: 'week',
};
...
}

Resources