Writing one redux Reducer and passing one initial object - reducers

I have gone through the Redux document and figured out that we can write only one reducer for whole app, if we are able to design the initial object.
If i am true, is this implementation is valid , i have adopted only one reducer and designed my object with my requirements.
This is my code
import merge from 'lodash/object/merge';
const initialState = {
user: {isLoggedIn: false, showSignIn: false, showSignUp: false},
exams: {},
subjects: {isfetched: false},
units: {},
classRooms: [],
pricePlansSelected: {},
plansActiveState: { tabActive: 'popular' },
trackCPCClick: {},
pricingPlans: {},
popularPlans: {isfetched: false},
customPlans: {isfetched: false},
unitsForCustomSelection: {},
appliedCoupon: { code: '', applied: false},
examAttempts: {isfetched: false},
examAttemptSelected: {}
};
export default function entities(state = initialState, action) {
return merge({}, state, action);
}

While it is technically true that Redux stores use one reducer, breaking up your reducers into readable and maintainable pieces is fundamental to good Redux development. The best way to do this is to create a new reducer function for each piece of state and then use combineReducers() to combine all of those pieces back into one reducer function.
Using a few pieces of your state, you could nicely divide it up like this:
import { combineReducers } from 'redux'
const userState = {isLoggedIn: false, showSignIn: false, showSignUp: false};
function userReducer(state = userState, action) {
//build new piece of state based on action here
}
const subjectState = {isfetched: false};
function subjectReducer(state = subjectState, action) {
//build new piece of state based on action here
}
const appState = combineReducers({
user: userReducer,
subject: subjectReducer
});
export default appState;
The main problem with the code that you have posted is that you need to account for all possible action types within your reducer functions. Otherwise your entire app could only ever have one action that rebuilt the whole app state with its data every time. You can account for different action types any way that you'd like, but the main convention has been to use a switch. Also note that while you use lodash merge for your state creation, you can also use Object.assign() or the spread operator (...three dots) to do the same thing, if you are compiling with something like babel.
So with that in mind, one of your reducer functions would look something like this:
function userReducer(state = userState, action) {
switch (action.type) {
case 'LOAD_USER':
return action.result; //assumes action.result is same structure as user piece of state
case 'LOG_IN':
return {...state, isLoggedIn: true}
default:
return state;
}
}
Because your reducers are broken up into maintainable pieces, your actions only have to return the data that corresponds to that particular piece of state and not the entire application.

Related

React component not receiving intermediate state when chaining actions in redux-saga

I have two actions TEST and TEST_DONE which both increment an id property in my redux state. I am using redux-saga to dispatch the second action TEST_DONE automatically whenever I dispatch the first action TEST from my component.
I expect the order of execution to go like this:
component renders with initial value of testState.id = 0
component dispatches TEST action
component re-renders with testState.id = 1
saga dispatches the TEST_DONE action
component re-renders with testState.id = 2
Instead my component only re-renders when testState.id is updated to 2. I can't see the 1 value in the getSnapshotBeforeUpdate function. It shows 0 as the previous prop.
Why does the prop jump from 0 to 2 without receiving 1 in between?
saga.js:
export function* TestSagaFunc() {
yield put({
type: actions.TEST_DONE
});
};
export default function* rootSaga() {
yield all([
yield takeEvery(actions.TEST, TestSagaFunc),
]);
};
action.js:
const actions = {
TEST: 'TEST',
TEST_DONE: 'TEST_DONE',
callTest: (id) => ({
type: actions.TEST,
payload: {
id
}
}),
};
export default actions;
reducer.js:
const initState = {
testState: {
id: 0
}
};
export default function TestReducers ( state=initState, { type, ...action}) {
switch(type) {
default:
return state;
case actions.TEST: {
const { id } = state.testState;
const nextId = id + 1;
return {
...state,
testState: {
...state.testState,
id: nextId
}
};
};
case actions.TEST_DONE: {
const { id } = state.testState;
const nextId = id + 1;
return {
...state,
testState: {
...state.testState,
id: nextId
}
};
}
};
};
console output from component getSnapshotBeforeUpdate
Summarizing my comments from the question:
The redux state is indeed being updated as you've seen, but a component is not guaranteed to render every intermediate state change based on the way react batches state changes. To test this you can try importing delay from redux-saga/effects and adding yield delay(1000); before calling yield put in TestSagaFunc so the two state updates don't get batched together.
This is just a trick to illustrate the effects of batching and almost certainly not what you want to do. If you need the intermediate state to be rendered you could dispatch TEST_DONE from the component being rendered with a useEffect (or componentDidUpdate) to ensure that the component went through one render cycle with the intermediate state. But there is no way to force your component to render intermediate reducer states that are batched together.

Passing multiple variable to Redux Action creator. What format?

I am pretty new to Redux but I am getting a good hang of it so far.
I am setting up an actions file and I would need to pass two values to payload.
Please let me know if the question has already been asked elsewhere and I missed it.
Thanks!
kramnic
p.s. below is what I have drafted
export const transferValues = (fromId, toId) => {
return {
type: VALUES_TRANSFER,
payload: { fromId, toId }, //should it be array? not sure about syntax here
};
};
you can also have an action of type { type: string, fromId: number, toId: number } instead of a single "payload". that's a matther of preference I think. you just need to adapt you reducers accordingly. I personally like the above attempt more because it represents single attributes like I have them in my state as well. so I can do like a one-to-one mapping. for example:
// state
export type SessionState = {
+id: number,
+username: string,
+sessionId: string,
}
// reducer here
[...]
case SET_USERNAME:
const setUsernameAction = ((action: any): SetUsernameAction)
return {
...state,
username: setUsernameAction.username,
sessionId: setUsernameAction.sessionId,
}
[...]
// example action
export const setUsername = (username: string, sessionId: string): SetUsernameAction => {
return { type: SET_USERNAME, username, sessionId }
}
but you will certainly never use an array for multiple attributes. you'll lose the named access to properties as well as any chance to type your action. stick with objects, no matther what solution you'll go for.

How to use most recent state from useReducer before re-render

I have two reducer actions that I want to dispatch one after the other. The first one modifies the state, then the second one uses a portion of the modified state to make another modification. The difficulty is that when the second dispatch is called, it still has the old outdated state and thus doesn't update the state properly.
An example is the following (also found here - https://codesandbox.io/s/react-usereducer-hqtc2) where there is a list of conversations along with a note of which one is considered the "active" conversation:
import React, { useReducer } from "react";
const reducer = (state, action) => {
switch (action.type) {
case "removeConversation":
return {
...state,
conversations: state.conversations.filter(
c => c.title !== action.payload
)
};
case "setActive":
return {
...state,
activeConversation: action.payload
};
default:
return state;
}
};
export default function Conversations() {
const [{ conversations, activeConversation }, dispatch] = useReducer(
reducer,
{
conversations: [
{ title: "James" },
{ title: "John" },
{ title: "Mindy" }
],
activeConversation: { title: "James" }
}
);
function removeConversation() {
dispatch({ type: "removeConversation", payload: activeConversation.title });
dispatch({ type: "setActive", payload: conversations[0] });
}
return (
<div>
Active conversation: {activeConversation.title}
<button onClick={removeConversation}>Remove</button>
<ul>
{conversations.map(conversation => (
<li key={conversation.title}>{conversation.title}</li>
))}
</ul>
</div>
);
}
In here, when I click the "remove conversation" button, I want to remove the active conversation, then set the active conversation to be the one at the top of the list. However, here when the first dispatch removes the conversation from the list, the second dispatch sets active to conversations[0], which still contains the removed value (since the state hasn't updated yet). As a result, it keeps the active conversation as the one it was before, even though it's been removed from the list.
I could probably combine the logic into just one action and do it all there (remove the conversation and set active all in one), but I would ideally like to keep my reducer actions to have one responsibility each if possible.
Is there any way to make the second dispatch call have the most recent version of the state so that this kind of problem doesn't occur?
It may help if you think of useEffect() like setState's second parameter (from class based components).
If you want to do an operation with the most recent state, use useEffect() which will be hit when the state changes:
const {
useState,
useEffect
} = React;
function App() {
const [count, setCount] = useState(0);
const decrement = () => setCount(count-1);
const increment = () => setCount(count+1);
useEffect(() => {
console.log("useEffect", count);
}, [count]);
console.log("render", count);
return (
<div className="App">
<p>{count}</p>
<button onClick={decrement}>-</button>
<button onClick={increment}>+</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render( < App / > , rootElement);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.6/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Some further info on useEffect()
Answering this for anyone who may come across similar issues in the future. The key to finding the solution to this is understanding that state in React is a snapshot.
You can see that in the dispatched setActive action, the value of conversations[0] of state is being passed:
dispatch({ type: "setActive", payload: conversations[0] });
Thus when the action is called before the next render, it uses the snapshotted state at the time of re-render:
// snapshot of state when action is called
{
conversations: [
{ title: "James" },
{ title: "John" },
{ title: "Mindy" }
],
activeConversation: { title: "James" }
}
Thus conversations[0] evaluates to {title: "James"}. This is why in the reducer, activeConversation: action.payload returns {title: "James"} and the active conversation doesn't change. In technical terms, "you're calculating the new state from the value in your closure, instead of calculating it from the most recent value."
So how do we fix this? Well useReducer actually in fact always has access to the most recent state value. It is a sister pattern to the state updater function, which also gives you access to the latest state variable even before the next render.
This means that after the first dispatch action:
dispatch({ type: "removeConversation", payload: activeConversation.title }); // first dispatch action
dispatch({ type: "setActive", payload: conversations[0] }); // second dispatch action
the next dispatch action actually has access to the latest state already. You just need to access it:
case "setActive":
return {
...state,
activeConversation: state.conversations[0]
};
You can verify this by logging it to the console:
const reducer = (state, action) => {
console.log(state);
switch (action.type) {
case "removeConversation":
return {
...state,
conversations: state.conversations.filter(
c => c.title !== action.payload
)
};
case "setActive":
return {
...state,
activeConversation: state.conversations[0]
};
default:
return state;
}
};
Also important to note that the 2 dispatch calls are batched as explained in the state updater function link mentioned above. More info on batching here too.

reselect across multiple comoponents work only with deepEqual check

I've tested in various ways... Still, It isn't working.
I don't seem to doing anything wrong
exactly same code as reselect doc
redux store is all normalized
reducers are all immutable
From parent component, I just pass down a prop with id and from child component, connected with redux and used selector to get that exact item by id(from parent component)
### This is what Parent components render looks like
render() {
return (
<div>
<h4>Parent component</h4>
{this.props.sessionWindow.tabs.map(tabId =>
<ChildComponentHere key={tabId} tabId={tabId} />
)}
</div>
);
}
### This is what Child component looks like
render() {
const { sessionTab } = this.props (this props is from connect() )
<div>
<Tab key={sessionTab.id} tab={sessionTab} />
</div>
))
}
### Selectors for across multiple components
const getTheTab = (state: any, ownProps: IOwnProps) => state.sessionWindows.sessionTab[ownProps.tabId];
const makeTheTabSelector = () =>
createSelector(
[getTheTab],
(tab: object) => tab
)
export const makeMapState = () => {
const theTabSelector = makeTheTabSelector();
const mapStateToProps = (state: any, props: IOwnProps) => {
return {
sessionTab: theTabSelector(state, props)
}
}
return mapStateToProps
}
Weirdly Working solution: just change to deep equality check.(from anywhere)
use selectors with deep equality works as expected.
at shouldComponentUpdate. use _.isEqual also worked.
.
1. const createDeepEqualSelector = createSelectorCreator(
defaultMemoize,
isEqual
)
2. if (!_isEqual(this.props, nextProps) || !_isEqual(this.state, nextState)){return true}
From my understanding, my redux is always immutable so when something changed It makes new reference(object or array) that's why react re-renders. But when there is 100 items and only 1 item changed, only component with that changed props get to re-render.
To make this happen, I pass down only id(just string. shallow equality(===) works right?)using this id, get exact item.(most of the components get same valued input but few component get different valued input) Use reselect to memoize the value. when something updated and each component get new referenced input compare with memoized value and re-render when something trully changed.
This is mostly what I can think of right now... If I have to use _isEqual anyway, why would use reselect?? I'm pretty sure I'm missing something here. can anyone help?
For more clarification.(hopefully..)
First,My redux data structure is like this
sessionWindow: {
byId: { // window datas byId
"windowId_111": {
id: "windowId_111",
incognito: false,
tabs: [1,7,3,8,45,468,35,124] // this is for the order of sessionTab datas that this window Item has
},
"windowId_222": {
id: "windowId_222",
incognito: true,
tabs: [2, 8, 333, 111]
},{
... keep same data structure as above
}
},
allIds: ["windowId_222", "windowId_111"] // this is for the order of sessionWindow datas
}
sessionTab: { // I put all tab datas here. each sessionTab doesn't know which sessionWindow they are belong to
"1": {
id: 1
title: "google",
url: "www.google.com",
active: false,
...more properties
},
"7": {
id: 7
title: "github",
url: "www.github.com",
active: true
},{
...keep same data structure as above
}
}
Problems.
1. when a small portion of data changed, It re-renders all other components.
Let's say sessionTab with id 7's url and title changed. At my sessionTab Reducer with 'SessionTabUpdated" action dispatched. This is the reducer logic
const updateSessionTab = (state, action) => {
return {
...state,
[action.tabId]: {
...state[action.tabId],
title: action.payload.title,
url: action.payload.url
}
}
}
Nothing is broken. just using basic reselect doesn't prevent from other components to be re-rendered. I have to use deep equality version to stop re-render the component with no data changed
After few days I've struggled, I started to think that the problem is maybe from my redux data structure? because even if I change one item from sessionTab, It will always make new reference like {...state, [changedTab'id]: {....}} In the end, I don't know...
Three aspects of your selector definition and usage look a little odd:
getTheTab is digging down through multiple levels at once
makeTheTabSelector has an "output selector" that just returns the value it was given, which means it's the same as getTheTab
In mapState, you're passing the entire props object to theTabSelector(state, props).
I'd suggest trying this, and see what happens:
const selectSessionWindows = state => state.sessionWindows;
const selectSessionTabs = createSelector(
[selectSessionWindows],
sessionWindows => sessionWindows.sessionTab
);
const makeTheTabSelector = () => {
const selectTabById = createSelector(
[selectSessionTabs, (state, tabId) => tabId],
(sessionTabs, tabId) => sessionTabs[tabId]
);
return selectTabById;
}
export const makeMapState() => {
const theTabSelector = makeTheTabSelector();
const mapStateToProps = (state: any, props: IOwnProps) => {
return {
sessionTab: theTabSelector(state, props.tabId)
}
}
return mapStateToProps
}
No guarantees that will fix things, but it's worth a shot.
You might also want to try using some devtool utilities that will tell you why a component is re-rendering. I have links to several such tools in the Devtools#Component Update Monitoring section of my Redux addons catalog.
Hopefully that will let you figure things out. Either way, leave a comment and let me know.

Update Redux Store

I am trying to update the redux store but when I try to access both points and sessionId, they come back undefined. I am sure there is a problem with my reducer, but I can't figure it out. Any help would be much appreciated.
Here's my reducer:
import { UPDATE_POINTS, SET_SESSION } from '../path'
const initialState = {
sessionId: null,
points: []
}
export default (state = initialState, action) => {
switch (action.type) {
case UPDATE_POINTS:
return {
points: action.points
}
case SET_SESSION:
return {
sessionId: action.session
}
default:
return state;
}
}
Edit:
Action Creators
export function updatePoints(points){
return {
type: UPDATE_POINTS,
points
}
}
export function setSession(session){
return {
type: SET_SESSION,
session
}
}
Within React Component (for simplicity I took most everything else out of this function)
handleSelect(e) {
this.props.setSession(e);
console.log(this.props.sessionId);
}
This function is used when a menu item is chosen from a drop down menu. On the first selection, the console shows whatever is in the initial state for sessionId. Any further drop down selections result in undefined in the console.
You're super close. A reducer in redux needs to return the a new copy of the entire state. Your reducer is returning only the key it's concerned with, which is going to drop the other key. You need to return a new copy of the state with your key updated. For example:
const initialState = {
sessionId: null,
points: []
}
export default (state = initialState, action = null) => {
// Exit early if you don't have an action (returning old state)
if (!action) return state;
// This function will assign your patch onto the old state, and then
// assign all of that onto a NEW object. For redux to do it's job,
// you can't modulate the old object, you have to return a new one.
const update = patch => Object.assign({}, state, patch);
switch (action.type) {
case UPDATE_POINTS:
return update({
points: action.points
});
case SET_SESSION:
return update({
sessionId: action.session
});
default:
return state;
}
}
And for the record, instead of putting your data payload under a unique key each time in your action creators, if you put the payload under a data key then your action will follow the standard flux action format.
export const updatePoints = (points) => ({
type: UPDATE_POINTS,
data: points
});
export const setSession = (session) => ({
type: SET_SESSION,
data: session
});
There you go. Good luck, and if you get stuck, refer back to the Redux docs (they're really good). Link to Redux Docs

Resources