Redux - approach to modifying state with actions - javascript

Hello i'm trying to create some kind of lottery and i'm wondering which approach of modifying state by actions payload should be used.
Let's say i have state
type initialCartState = {
productsFromPreviousSession: Product[]
selectedProduct: Product
balance: number,
productsInCart: Product[]
}
and our reducer looks like
const reducers = {
addProduct(state, action) => {
state.products.push(state.action.payload.product)
},
addProductsFromPreviousSession(state, action) => {
state.products.push(...state.productsFromPreviousSession)
},
}
And i noticed i used completely two different approaches with these two types cuz in my component it looks like
const component = () => {
const selectedProduct = useSelector(state => state.cart.selectedProduct);
const availableBalance = useSelector(state => state.cart.balance - sum(state.cart.products, 'price'));
const dispatch = useDispatch()
const sumOfProductsFromPreviousSession = useSelector(state => sum(state.cart.products,'price'))
return (
<div>
<div onClick={() => {
if((balance - selectedProduct.price) > 0) {
dispatch(cartActions.addProduct(selectedProduct))
}
}}/>
<div onClick={() => {
if((balance - sumOfProductsFromPreviousSession) > 0) {
dispatch(cartActions. addProductsFromPreviousSession())
}
}}/>
</div>
)
}
There are two different types of handling actions, in addProduct i used selector and pass value in action payload. In Add products from previous session we rely on state inside reducer (Also have middleware for purpose of saving in localStorage, but there i used store.getState()). Which kind of approach is correct ?
Also how it will change when we move balance to another reducer, and then we will not have access to that i cartReducer?
I saw there are bunch of examples on counter when increment and decrement rely on current reducerState and there are actions without payload, but there is no validation which is used in my example.
Thanks in advance !

Both approaches can be used. Basically, if you need to show state data in UI or other parts of processes, you should read with selector. This way, changes inside the store can be reflected in the components reactively.
In your case, you are just updating the state value with currently available data from the state. So, you can dispatch action without payload.
In your example, even though you pass the payload from onClick event, you are still reading the value from the state itself.

Related

How to prevent useSelector from causing unnecessary renders?

I'm using useSelector hook to retrieve the values of my reducer, but it is causing a lot of unnecessary renders on my application.
It doesn't matter which property I'm using on a component, since they are all getting the same state object from the reducer, every time one property changes, all components that use useSelector are rendered.
This is the reducer:
const initialState = {
productsDataState: [], // => this is populated by multiple objects
searchProducts: [],
isSearchOn: false,
inputValue: '',
listOrder: [],
toastify: ['green', ''],
toastifyOpen: false
}
const reducer = ((state = initialState, action) => {
switch (action.type) {
case actionTypes.UPDATE_PRODUCT:
return {
...state,
productsDataState: action.products,
listOrder: action.listOrder
}
case actionTypes.SET_TOASTIFY:
return {
...state,
toastify: action.toastify,
toastifyOpen: action.open
}
case actionTypes.SET_SEARCH:
return {
...state,
searchProducts: action.searchProducts,
isSearchOn: action.isSearchOn,
inputValue: action.inputValue
}
default:
return state
}
})
One of the components is using isSearchOn, which is a boolean, so I solved the problem checking if the value is true before rendering it:
const { isSearchOn } = useSelector(state => state.isSearchOn && state)
But that's not the case for all components. The one I'm stuck right now uses the productsDataState property, which is an array of objects. I can't just make a simple validation before returning state. I thought about storing the initial value in a useState, make a deep comparison between the current value and the past one before returning the state, which would work similarly to what I did in the other component, but I can't see how this would be a good approach.
const { productsDataState } = useSelector(state => state)
Is there a way where I could take advantage of useSelector without comprimising the performance of the application?
I've being reading a lot and making a lot of tests, but I couldn't find a good way to do that so far.
I'd like to keep useSelector, but I'm open to suggestions, even if it involves other libraries.
What you should be doing is not selecting whole state, just the part you need :)
const productsDataState = useSelector(state => state.productsDataState)
#edit
If you want to select multiple data with one selector you will cause it to change reference if you would try to use an object for example.
const { productsDataState, other } = useSelector(state => ({ productsDataState: state.productsDataState, other: state.other }))
this will cause rerender on any state change as redux use strict equality check by default.
You should listen to official documentation and select each state separately
Call useSelector() multiple times, with each call returning a single field value

Using redux to take a time on a stopwatch and put it into a database

I was recommended using redux to place a time into a database but I'm having issues. I have a stopwatch in my index.js that the user can start and stop. After, I have a button that allows them the ability to to put their time into a database. From my node file, I'm getting UnhandledPromiseRejectionWarning: error: null value in column "time" violates not-null constraint. I'm wondering if I'm getting that because I have difference = 0 at the stop of the index file and it doesn't retrieve difference = this.state.endTime - this.state.startTime; or if there is another issue I'm not seeing.
index.js
export let difference = 0;
export default class Home extends React.Component {
constructor(props);
super(props);
this.state = {
startTime: 0,
endTime: 0,
}
}
handleStart = () => {
this.setState({
startTime: Date.now()
})
}
handleStop = () => {
this.setState({
endTime: Date.now()
})
}
render() {
difference = this.state.endTime - this.state.startTime;
return (
<App />
<reducer />
);
}
}
reducer.js
import * as actionTypes from "./actions.js";
import difference from "../pages/index.js";
const initialState = {
count: 0
}
const reducer = (state = initialState, action) => {
switch (action.type) {
case actionTypes.NUMBER:
return { state, count: difference };
default: return state;
}
};
export default reducer;
store.js
import { createStore } from 'redux';
import reducer from './reducer';
const store = createStore(reducer);
export default store;
actions.js
export const NUMBER = 'NUMBER';
App.js
import React from 'react';
import { addName } from "./util";
function App() {
const [name, setName] = React.useState("")
function handleUpdate(evt) {
setName(evt.target.value);
}
async function handleAddName(evt) {
await addName(name);
}
return <div>
<p><input type='text' value={name} onChange={handleUpdate} /></p>
<button className='button-style' onClick={handleAddName}>Add Name</button>
</div>
}
export default App;
util.js
import "isomorphic-fetch"
import difference from "../pages/index.js";
export function addName(name) {
return fetch('http://localhost:3001/addtime', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, time: difference })
})
}
node server.js
app.post("/addtime", cors(), async (req,res) => {
const name = req.body.name;
const time = req.body.time;
const timeStamp = dateFormat(time, dateFormat.masks.isoDateTime);
const template = 'INSERT INTO times (name,time) VALUES ($1,$2)';
const response = await pool.query(template, [name,time]);
res.json({name: name, time: time});
});
State Management
In React we manage changing values through state. There is a way to access the value and a way to update the value. When the value changes, React knows to re-render the component.
This state can be stored within a component of your app. When you write const [name, setName] = React.useState(""), name is your "getter" and setName is your "setter".
The state can also be stored inside a Context Provider component that is a parent of the current component. Any children of this Provider can use the useContext hook to access the stored value.
The React Redux package uses these React contexts to make the redux state globally available. You wrap your entire App in a Redux Provider component to enable access.
With Redux, your "getters" are selector functions, which you call though useSelector, and your "setters" are actions that you pass though the dispatch function. React actually supports this syntax of state management on a local component level (without Redux) though the useReducer hook.
You don't need to use contexts in order to pass around state from component to component. You can also pass values and "setters" as props from a parent component to a child. React recommends this approach and has a section on Lifting State Up.
This is just a brief overview of some of the concepts at play. The issues that you are having here are due to state management. There are many possible ways to handle your state here. In my opinion Redux and Contexts are both unnecessary overkill. Of course you can use them if you want to, but you need to set them up properly which you haven't done.
Errors
export let difference = 0;
Variables which exist at the top-level of a file, outside of a component, should be immutable constants only. When you have a value that changes, it needs to be part of your app state.
When you have a "stateful" value like difference you can't just use a variable that exists outside of your components.
render() {
difference = this.state.endTime - this.state.startTime;
...
This is not how you update a value. We don't want to be just setting a constant. We also don't want to trigger changes of a stateful value on every render as this creates infinite re-render situations.
handleStart & handleStop
The functions themselves are fine but they are never called anywhere. Therefore difference will always be 0.
<reducer />
A reducer is not a component so you cannot call it like this. It is a pure function. In order to introduce redux into your JSX code, you use the reducer to create a store variable and you pass that store to a react-redux Provider component.
const reducer = (state = initialState, action) => {
switch (action.type) {
case actionTypes.NUMBER:
return { state, count: difference };
default: return state;
}
};
You want to include the difference as a property of your action rather than accessing an external variable. The standard convention is to add a payload property to the action. The payload could be the difference number itself or an object with a property difference. That sort of design choice is up to you. An action might look like { type: actionTypes.NUMBER, payload: 0.065 } or { type: actionTypes.NUMBER, payload: { difference: 0.065 } }.
Your state is an object with a property count. Your reducer should return the next state, which should also be an object with a property count. This will not return the right state shape: return { state, count: difference };.
It is typical to use the spread operator ... to copy all other properties of the state and update just one (or a few), like this: return { ...state, count: difference }. However your state does not have any other properties, so that is the same as return { count: difference };. Since you are just storing a single number, there is no value from the previous state that is copied or preserved. (Which is a large part of why I think that Redux is not helpful or necessary here.)
There may be some issues on the backend as well, but there are such serious issues with the front end that I think that's your main problem.
Structuring Components
Think about what your app needs to do. What actions does it respond to? What information does it need to know?
Based on your description, it needs to:
Start a timer when a button is clicked
Stop a timer when a button is clicked
Store the most recent timer value
Allow a user to enter and update a name
Store that name
POST to the backend when a button is clicked, if there is a stored difference and name
Next you break that up into components. Store each stateful value at the highest component in the tree which needs to access it, and pass down values and callbacks as props. The startTime and endTime are only used by the timer itself, but the timer need to set the difference after stopping. The difference would be stored higher up because it is also needed for the POST request.
Think about what buttons and actions are available at any given time, and what conditions are used to determine this. For example, you shouldn't be able to stop a timer before you have started it. So we would see if startTime is greater than 0.
Minimal Front-End Example
import * as React from "react";
const Timer = ({ setDifference }) => {
const [startTime, setStartTime] = React.useState(0);
const handleStart = () => {
setStartTime(Date.now());
};
const handleStop = () => {
const difference = Date.now() - startTime;
setDifference(difference);
};
return (
<div>
<button onClick={handleStart}>
Start
</button>
<button disabled={startTime === 0} onClick={handleStop}>
Stop
</button>
</div>
);
};
const Submission = ({ time }) => {
const [name, setName] = React.useState("");
function handleUpdate(evt) {
setName(evt.target.value);
}
async function handleAddName(evt) {
return await fetch("http://localhost:3001/addtime", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name, time })
});
}
return (
<div>
<div>Time: {time}</div>
<input type="text" value={name} onChange={handleUpdate} />
<button onClick={handleAddName} disabled={name.length === 0}>
Add Name
</button>
</div>
);
};
const App = () => {
const [time, setTime] = React.useState(0);
return (
<div>
<Timer setDifference={setTime} />
{time > 0 && <Submission time={time} />}
</div>
);
};
export default App;
CodeSandbox Link
You might not be saving any value to DOB column, you need to allow NULL value. You should change to
DOB = models.DateField(auto_now = False, null = True)
You should also make sure DOB column in your table allows null.

Managing multiple calls to the same Apollo mutation

So taking a look at the Apollo useMutation example in the docs https://www.apollographql.com/docs/react/data/mutations/#tracking-loading-and-error-states
function Todos() {
...
const [
updateTodo,
{ loading: mutationLoading, error: mutationError },
] = useMutation(UPDATE_TODO);
...
return data.todos.map(({ id, type }) => {
let input;
return (
<div key={id}>
<p>{type}</p>
<form
onSubmit={e => {
e.preventDefault();
updateTodo({ variables: { id, type: input.value } });
input.value = '';
}}
>
<input
ref={node => {
input = node;
}}
/>
<button type="submit">Update Todo</button>
</form>
{mutationLoading && <p>Loading...</p>}
{mutationError && <p>Error :( Please try again</p>}
</div>
);
});
}
This seems to have a major flaw (imo), updating any of the todos will show the loading state for every single todo, not just the one that has the pending mutation.
And this seems to stem from a larger problem: there's no way to track the state of multiple calls to the same mutation. So even if I did want to only show the loading state for the todos that were actually loading, there's no way to do that since we only have the concept of "is loading" not "is loading for todo X".
Besides manually tracking loading state outside of Apollo, the only decent solution I can see is splitting out a separate component, use that to render each Todo instead of having that code directly in the Todos component, and having those components each initialize their own mutation. I'm not sure if I think that's a good or bad design, but in either case it doesn't feel like I should have to change the structure of my components to accomplish this.
And this also extends to error handling. What if I update one todo, and then update another while the first update is in progress. If the first call errors, will that be visible at all in the data returned from useMutation? What about the second call?
Is there a native Apollo way to fix this? And if not, are there options for handling this that may be better than the ones I've mentioned?
Code Sandbox: https://codesandbox.io/s/v3mn68xxvy
Admittedly, the example in the docs should be rewritten to be much clearer. There's a number of other issues with it too.
The useQuery and useMutation hooks are only designed for tracking the loading, error and result state of a single operation at a time. The operation's variables might change, it might be refetched or appended onto using fetchMore, but ultimately, you're still just dealing with that one operation. You can't use a single hook to keep track of separate states of multiple operations. To do that, you need multiple hooks.
In the case of a form like this, if the input fields are known ahead of time, then you can just split the hook out into multiple ones within the same component:
const [updateA, { loading: loadingA, error: errorA }] = useMutation(YOUR_MUTATION)
const [updateB, { loading: loadingB, error: errorB }] = useMutation(YOUR_MUTATION)
const [updateC, { loading: loadingC, error: errorC }] = useMutation(YOUR_MUTATION)
If you're dealing with a variable number of fields, then we have to break out this logic into a separate because we can't declare hooks inside a loop. This is less of a limitation of the Apollo API and simply a side-effect of the magic behind hooks themselves.
const ToDo = ({ id, type }) => {
const [value, setValue] = useState('')
const options = { variables = { id, type: value } }
const const [updateTodo, { loading, error }] = useMutation(UPDATE_TODO, options)
const handleChange = event => setValue(event.target.value)
return (
<div>
<p>{type}</p>
<form onSubmit={updateTodo}>
<input
value={value}
onChange={handleChange}
/>
<button type="submit">Update Todo</button>
</form>
</div>
)
}
// back in our original component...
return data.todos.map(({ id, type }) => (
<Todo key={id} id={id} type={type] />
))

Adding a variable to the store upon a functional component's initial render

Let's assume we want a Counter component <Counter startingValue={0}/> that allows us to specify the starting value in props and simply increases upon onClick.
Something like:
const Counter = (props: {startingValue: number}) => {
const dispatch = useDispatch();
const variable = useSelector(store => store.storedVariable);
return <p onClick={dispatch(() = > {storedVariable: variable})}>{variable}</p>;
}
Except, as it mounts, we'd like it to store its counting variable in the redux store (its value equal to the startingValue prop) and, as it unmounts, we'd like to delete the variable from the store.
Without the store, we could simply use the useState(props.startingValue) hook, however with the store it seems like we need constructors / equivalent.
A solution I see is to implement a useState(isInitialRender) variable and to create a variable in the store or not basing on an if instructor, albeit it looks like a bit convoluted solution to me.
I also get the feeling that I'm trying to do something against the react-redux philosophy.
This is the sort of thing that useEffect is intended for. If you specify an empty array for the second argument (the dependency array) then it will only run on the first render, and you can use the return function to remove it.
Here's roughly how to do it:
const Counter = (props: {startingValue: number}) => {
const dispatch = useDispatch();
const variable = useSelector(store => store.storedVariable);
useEffect(() => {
dispatch({type: 'store-starting-value', payload: startingValue})
return ()=>{
dispatch({type: 'clear-starting-value'})
}
}, []);
//...

Redux reducer - on state change

I want to save the current state of a specific reducer into session storage without explicitly calling it on every action.
const { handleActions } = require("redux-actions");
// How do i make this logic run on any state change without adding it to every action?
const onStateChange = (state) => sessionStorage.save(keys.myReducer, state);
const myReducer = handleActions
({
[Actions.action1]: (state, action) => (
update(state, {
prop1: {$set: action.payload},
})
),
[Actions.action2]: (state, action) => (
update(state, {
prop2: {$set: action.payload},
})
)
},
//****************INITIAL STATE***********************
{
prop1: [],
prop2: []
}
);
Is there a way to catch the state change event for a specific reducer?
I know of store.subscribe, but that's for the entire store, i'm talking about listening to a specific reducer change
Thanks
Unfortunately you can't watch specific parts of your store because your store is actually an only reducer (combineReducers return a single big reducer). That said you can manually check if your reducer has changed by doing state part comparison.
let previousMyReducer = store.getState().myReducer;
store.subscribe(() => {
const { myReducer } = store.getState();
if (previousMyReducer !== myReducer) {
// store myReducer to sessionStorage here
previousMyReducer = myReducer;
}
})
You can use redux-saga to listen for specific actions and persist the store (and do your other logice) when they are fired. This seems like a good fit for your needs.
It's not exactly tied to specific store changes, but rather to a set of actions. However, I think this model (listening for actions, as opposed to state changes) is better suited to the redux design.
The nice thing is you don't have to change any code outside of the saga. I've used this one for autosaving form data and it worked great.
Here's an example of setting up a saga to persist on a few redux-form actions; note that it's not limited to persisting - you can do whatever you want during a saga.
function* autosaveSaga(apiClient) {
yield throttle(500,
reduxFormActionTypes.CHANGE,
saveFormValues,
apiClient);
yield takeLatest(
reduxFormActionTypes.ARRAY_PUSH,
saveFormValues,
apiClient);
yield takeLatest(
reduxFormActionTypes.ARRAY_POP,
saveFormValues,
apiClient);
}
If you want to listen to all action types, or do custom filtering of which actions fire your persistence code, you can pass a pattern, or a matching function to takeLatest:
yield takeLatest(
'*',
saveFormValues,
apiClient);
More on what you can pass to take, takeLatest etc can be found in the docs for take.

Categories

Resources