Redux: how to define logic between states? - javascript

Given the following 2 states as an example:
state_a: 'mode_1' | 'mode_2' | 'mode_3'
state_b: boolean
state_b can only be false whenever state_a === 'mode_2', and when state_a !== 'mode_2', state_b should return to the previous value (i.e. it's limits opened up. For example, if it was true -> false (limited) -> true or false -> false (limited) -> false).
What's the usual practice/style to define such a behavior in Redux?

If I'm understanding your question correctly you want, from the app's perspective, state_b to always be false when state_a === 'mode_2', and when state_a !== 'mode_2' state_b is whatever is stored in state.
On the surface your questions is posed in such a way that it sounds like you want to implement some logic in the reducer functions that coordinates the values between these two states when either of them update. While you could do this I suspect a simpler solution is to derive the provided state when consuming it. In other words, use a selector function to compute derived state, when the state_a value is "mode_2" then the selector function selecting state_b returns false, otherwise it returns the actual state_b state value.
Example:
import {
combineReducers,
configureStore,
createSlice,
createSelector
} from "#reduxjs/toolkit";
import { Provider, useDispatch, useSelector } from "react-redux";
const MODES = {
mode_1: "mode_1",
mode_2: "mode_2",
mode_3: "mode_3"
};
const state = createSlice({
initialState: {
state_a: MODES.mode_1,
state_b: true
},
name: "state",
reducers: {
setMode: (state, action) => {
state.state_a = action.payload;
},
toggleB: (state, action) => {
state.state_b = !state.state_b;
}
}
});
const { setMode, toggleB } = state.actions;
const selectState = (state) => state.state;
const select_a = createSelector([selectState], (state) => state.state_a);
const select_b = createSelector([select_a, selectState], (state_a, state) =>
state_a !== MODES.mode_2 ? state.state_b : false
);
Selecting the state in a component:
const state_a = useSelector(select_a);
const state_b = useSelector(select_b);
Demo:

You are looking for Sharing data between slice reducers,
import { combineReducers, createStore } from "redux";
type StateA = 'mode_1' | 'mode_2' | 'mode_3';
const stateAReducer = (state: StateA = 'mode_1', action) => {
switch (action.type) {
case 'SWITCH_MODE':
return action.payload;
default:
return state;
}
}
type StateB = boolean;
const stateBReducer = (state: StateB = true, action) => {
return state;
}
const combinedReducer = combineReducers({
stateA: stateAReducer,
stateB: stateBReducer
});
type CrossSliceState = ReturnType<typeof combinedReducer>;
const crossSliceReducer = (state: CrossSliceState, action) => {
switch (action.type) {
case 'SWITCH_MODE':
const nextStateA = stateAReducer(state.stateA, action);
return {
stateA: nextStateA,
stateB: nextStateA === 'mode_2' ? false : state.stateB
}
default:
return state;
}
}
const rootReducer = (state, action) => {
const intermediateState = combinedReducer(state, action);
const finalState = crossSliceReducer(intermediateState, action);
return finalState
}
const store = createStore(rootReducer);
store.subscribe(() => {
console.log(store.getState());
})
store.dispatch({ type: 'SWITCH_MODE', payload: 'mode_1' });
store.dispatch({ type: 'SWITCH_MODE', payload: 'mode_2' });
store.dispatch({ type: 'SWITCH_MODE', payload: 'mode_1' });
Output:
{ stateA: 'mode_1', stateB: true }
{ stateA: 'mode_2', stateB: false }
{ stateA: 'mode_1', stateB: false }

Related

Redux action to reset - states are equal

I have the following initialState For React Redux:
const inistialStateRedux = {
configuredFilters: {
data: {
countries: [],
divisions: [],
companies: [],
locations: [],
fields: [],
search: '',
},
},
};
Now I want to create a RESET reducer.
It looks like that:
export const createReducer = (initialState, handlers) => (
state = initialState,
action
) => {
if (action.type in handlers) {
return handlers[action.type](state, action);
}
return state;
};
export const multiUse = (reducer, name = '') => (state = null, action) => {
if (action.name !== name) return state;
return reducer(state, action);
};
import {
createReducer
} from '../helper';
import * as Action from './actions';
import inistialStateRedux from '../inistialStateRedux';
export default createReducer({
data: {},
}, {
[Action.RESET_CONFIGURED_FILTERS]: (state) => ({
...state,
data: {
...inistialStateRedux.configuredFilters.data,
},
}),
});
But Redux Devtools shows, that the states are equal. What am I doing wrong ?
In the Actions you can use a dispatcher to reset the Form.
Like this:
import axios from 'axios';
import { ADD} from '../modelType';
import { reset } from 'redux-form';
export const add= formValues => async dispatch => {
const res = await axios.post('/api/model/', { ...formValues });
dispatch({
type: ADD,
payload: res.data
});
dispatch(reset('yourUniqueFormName'));
};

Reset state to initial with redux-toolkit

I need to reset current state to initial state. But
all my attempts were unsuccessful. How can I do it using redux-toolkit?
const showOnReviewSlice = createSlice({
name: 'showOnReview',
initialState: {
returned: [],
},
reducers: {
reset(state) {
//here I need to reset state of current slice
},
},
});
Something like this:
const intialState = {
returned: []
}
const showOnReviewSlice = createSlice({
name: 'showOnReview',
initialState,
reducers: {
reset: () => initialState
}
});
This worked for me (mid-late 2020). Formatted with your code context as an example.
const initialState = {
returned: [],
};
const showOnReviewSlice = createSlice({
name: 'showOnReview',
initialState,
reducers: {
reset: () => initialState,
},
});
Replacing state with initialState directly did not work for me (mid 2020). What I finally got working was to copy each property over with Object.assign(). This worked:
const showOnReviewSlice = createSlice({
name: 'showOnReview',
initialState: {
returned: []
},
reducers: {
reset(state) {
Object.assign(state, initialState)
}
}
});
When using multiple slices, all slices can be reverted to their initial state using extraReducers.
First, create an action that can be used by all slices:
export const revertAll = createAction('REVERT_ALL')
In every slice add an initialState, and an extraReducers reducer using the revertAll action:
const initialState = {};
export const someSlice = createSlice({
name: 'something',
initialState,
extraReducers: (builder) => builder.addCase(revertAll, () => initialState),
reducers: {}
});
The store can be created as usual:
export const store = configureStore({
reducer: {
someReducer: someSlice.reducer,
}
})
And in your react code you can call the revertAll action with the useDispatch hook:
export function SomeComponent() {
const dispatch = useDispatch();
return <span onClick={() => dispatch(revertAll())}>Reset</span>
}
In my case, as the previous answer, mid 2021, just setting the initial state DO NOT WORK, even if you use the toolkit adapter like :
reducers: {
// Other reducers
state = tasksAdapter.getInitialState({
status: 'idle',
error: null,
current: null
})
}
},
instead, you should use Object.assign(), guess that it's related with the internal immer library behavior
We do it like this guys.
Suppose you want to clear all the data at the point of logging out.
In your store.tsx file:
import { AnyAction, combineReducers, configureStore } from '#reduxjs/toolkit';
import authReducer from './slices/authSlice'
import messageReducer from './slices/messageSlice'
const appReducer = combineReducers({
auth: authReducer,
message: messageReducer,
});
const reducerProxy = (state: any, action: AnyAction) => {
if(action.type === 'logout/LOGOUT') {
return appReducer(undefined, action);
}
return appReducer(state, action);
}
export const store = configureStore({
reducer: reducerProxy,
});
Then you create a thunk like this:
export const logout = createAsyncThunk(
"auth/logout",
async function (_payload, thunkAPI) {
thunkAPI.dispatch({ type: 'logout/LOGOUT' });
console.log('logged out')
}
);
You can use spread opearator for initialState
const initialState: {
returned: unknown[] //set your type here
} = {
returned: []
}
const showOnReviewSlice = createSlice({
name: 'showOnReview',
initialState,
reducers: {
reset() {
return {
...initialState
}
}
}
});
Try this. In my case, I wanted to return all slices to initialState when a certain action is dispatched.
First, let's create action:
import { createAction } from '#reduxjs/toolkit';
export const resetPanelsAction = createAction('resetPanelsData');
When creating our store, we save a copy of the initialState in the middleware:
import { Middleware } from '#reduxjs/toolkit';
export const resetDataMiddleware: Middleware =
({ getState }) =>
(next) => {
// "Caching" our initial app state
const initialAppState = getState();
return (action) => {
// Let's add the condition that if the action is of
// type resetData, then add our cached state to its payload
if (action.type === 'resetData') {
const actionWithInitialAppState = {
...action,
payload: initialAppState,
};
return next(actionWithInitialAppState);
}
return next(action);
};
};
Almost done! Now let's change our root reducer a little by adding a wrapper that will check the action type, and if it is equal to resetData, then return combinedReducers with our initialState, which will be in payload.
import { AnyAction } from 'redux';
import { combineReducers } from '#reduxjs/toolkit';
export const combinedReducers = combineReducers({
/** Your reducers */
});
export const rootReducer = (
state: ReturnType<typeof combinedReducers> | undefined,
action: AnyAction,
) => {
if (action.type === 'resetPanelsData') {
return combinedReducers(action.payload, action);
}
return combinedReducers(state, action);
};

React Redux - boolean value is undefined

I want process a plain boolean value with my Redux store. But this value results into undefined on every dispatch event. Does any of you see why?
I have the following setup:
reducer.js
const initialState = {
canSwipe: true
};
export default function rootReducer(state = initialState, action) {
switch (action.type) {
case CAN_SWIPE: {
console.log(action.payload.canSwipe) // This logs true or false - works!
return action.payload.canSwipe
}
default:
return state;
}
}
actions.js
export const setSwipeState = canSwipe => ({ type: CAN_SWIPE, payload: { canSwipe } });
component for dispatching
function MapOverlay({setSwipeState}) {
const zoom = 5;
return (
<Map
onMovestart={() => setSwipeState(false)}
onMoveend={() => setSwipeState(true)}
>
{...}
</Map>
)
}
export default connect(null, {setSwipeState})(MapOverlay);
The reducer should return data which must be compliant with your initial state:
const initialState = {
canSwipe: true
};
So you need to change your reducer to respect this structure:
switch (action.type) {
case CAN_SWIPE: {
console.log(action.payload.canSwipe) // This logs true or false - works!
return {canSwipe: action.payload.canSwipe}
}

How can I filter by ID and by name and city in react native with redux?

If I fetch this array of restos with redux:
[{
res_id: Int,
res_name: String,
res_category: String,
res_category_id: Int,
city_id: Int
}]
My action looks something like this:
export const getrestos = () => {
const resData = await response.json();
dispatch({
type: GET_RESTOS,
payload: resData
});
};
};
export const setFilters = filterSettings => {
console.log(filterSettings);
return { type: SET_FILTERS, filters: filterSettings };
};
And this is my reducer:
import { GET_RESTOS, SET_FILTERS } from '../actions/restos';
const initialState = {
restoList: [],
filteredRestos: []
};
export default (state = initialState, action) => {
switch (action.type) {
case GET_RESTOS:
return {
restoList: action.payload
}
case SET_FILTERS:
const appliedFilters = action.filters;
const updatedFilteredRestos = state.restoList.filter(resto => {
if (appliedFilters.cityID || resto.city_id) {
resto => resto.city_id.indexOf(cityID) >= 0
return { ...state, filteredRestos: updatedFilteredRestos };
}
});
return { ...state, filteredRestos: updatedFilteredRestos };
default:
return state;
}
};
I have touchable categorys in a page, and when i touch one i want to fetch the corresponding restos for that category and show them in a flatlist. Apart from that i want to have a search bar that when I type I want to show restos by res_name and/or by res_category.
Ive tried to create selectors, but I dont understand how, i dont need an specific approach, but the most clean or efficient as possible.
Thanks in advance if anyone can give me a hint or solution!
EDIT
The problem is im getting undefined in updatedFilteredRestos.
Your reducers should be clean, dumb and all they do should be returning objects. This makes your components more testable and errors easier to catch. In my opinion, this is a perfect use-case for reselect. Here's a medium article: https://medium.com/#parkerdan/react-reselect-and-redux-b34017f8194c But the true beauty of reselect is that it will memoize for you, i.e. if your states don't change, it uses a cached version of the data.
Anyway, you should clean up your restoReducer to something to this effect.
import { GET_RESTOS, SET_FILTERS } = "../actions/restos";
const initialState = {
restoList: [],
filteredRestos: []
};
const restoReducer = (state = initialState, action) => {
switch(action.type) {
case GET_RESTOS:
return { ...state, restoList: action.payload };
case SET_FILTERS:
return { ...state, filteredRestos: action.payload };
default:
return state;
}
}
Then write your filtered resto selector:
// ../selectors/restos
import { createSelector } from "reselect";
// First, get your redux states
const getRestos = (state) => state.restos.restoList;
const getFilteredRestos = (state) => state.restos.filteredRestos;
// Next, create selectors
export const getFilteredRestoList = createSelector(
[getRestos, getFilteredRestos],
(restoList, filteredRestos) => {
// need to check for non-empty filters
// if it is, simply return the unfiltered `restoList`
if(!Array.isArray(filteredRestos) || !filteredRestos.length)
return restoList || [];
// If you do have valid filters, return filtered logic
return restoList.filter(r => filteredRestos.some(f => f.cityID === r.city_id));
);
Then, use this selector in your components:
// ../components/my-app
import { getFilteredRestoList } from "../selectors/restos";
// hook it up to your `mapStateToProps` as you would a normal state
// except this time, it's a special selector
const mapStateToProps = (state, ownProps) => {
restoList: state.restos.restoList,
filteredRestos: state.restos.filteredRestos,
filteredRestoList: getFilteredRestoList(state) //<-- this is your selector
}
Then inside your component, just reference it: this.props.filteredRestoList.

it is necessary to implement the case REHYDRATE in redux-persist?

I am working on a proof of concept in react-native and I am using redux-persist. According to what I read the states are automatically assigned in version 5, however I have not managed to rehydrate the state without the case REHYDRATE.
My reducer:
import {ADD_USER} from '../constants/actionTypes';
import { REHYDRATE } from 'redux-persist';
const initialState = {
userName: null,
fetching: false
};
const userReducer = (state = initialState, action) => {
let copied = Object.assign({}, state);
switch (action.type){
case ADD_USER:
copied.userName = action.payload;
break;
case REHYDRATE:
//console.log('Payload desde el reducer ', action.payload);fsdffdsdsfsdfsdf
copied = action.payload.userReducer;
break;
default:
break;
}
return copied;
};
export default userReducer;
My configureStore
const persistConfig = {
key: 'root',
storage,
stateReconciler: autoMergeLevel2,
};
function logger({ getState }) {
return (next) => (action) => {
let returnValue = next(action)
return returnValue
}
}
const persistedReducer = persistCombineReducers(persistConfig, {userReducer});
export default () => {
let store = createStore(persistedReducer, undefined, applyMiddleware(logger));
let persistor = persistStore(store, null, () => {
Alert.alert(
'Current state',
JSON.stringify(store.getState()),
[
{text: 'OK', onPress: () => console.log('OK Pressed')},
],
{ cancelable: false }
);
});
return { store, persistor }
}
My problem was that I returned the state at the end of the method instead of returning it in each case of the switch and this surely broke the auto rehydration. Changing the reducer returns my problem disappeared.
The reducer code was like this:
const initialState = {
userName: null,
fetching: false
};
const userReducer = (state = initialState, action) => {
switch (action.type){
case ADD_USER:{
let copied = Object.assign({}, state);
copied.userName = action.payload;
return copied;
}
default:
return state;
}
};
export default userReducer;

Categories