I'm trying to learn how to implement a list in react. The docs recommend that I use the hooks API to use state. I read through them, and I have an idea of how to implement one:
function ListReducer(list, action) {
switch(action.type) {
case 'push': {
listCopy = [...list]
listCopy.push(action.data);
return listCopy
}
}
}
const [state, dispatch] = useReducer(ListReducer, []);
However, this strikes me as very inefficient, since the entire list needs to be copied in order to return the value, and generally this only takes O(1). Is there a better way to handle this situation?
Since you're using React, there is no way to change the state of an array without iterating over the entire array - you can't, for example, just .push to the existing array, because that'd mutate the state.
There is no definitively better option than your general approach - but you can make the syntax look a bit nicer:
case 'push':
return [...list, action.data];
The efficiency of a given section of code rarely matters in real-world projects, usually. There are often one or a few sections of code that are bottlenecks, but the likelyhood that any given section you're looking at at the moment is the bottleneck is slim.
Don't worry about it until you see that your app is running slower than desirable, and when that happens, run a performance analysis to see what exactly the bottleneck is, and fix that - until then, it's often not worth taking the time to worry about.
that's the nature of using the state/reducer pattern, nothing out of the box in the JavaScript language to change/handle that...
there's a library called immer, the basic idea is that with Immer you will apply all your changes to a temporary draft, which is a proxy of the currentState. Once all your mutations are completed, Immer will produce the nextState based on the mutations to the draft state,
turning your code in:
import produce from "immer"
function ListReducer(list, action) {
switch(action.type) {
case 'push': {
let nextState = produce(list, draftState => {
draftState.push(action.data)
})
return nextState
}
}
}
const [state, dispatch] = useReducer(ListReducer, [])
when using immer, there's a significant reduction in the maintenance for your team on "are we using mutable/immutable APIs in JavaScript?"... with immer, you remove this problem
Related
Why this works
const handleToggle = (id) => {
const newTodos = [...todos]
newTodos.map(todo => {
if (todo.id === id) {
todo.completed = !todo.completed
}
});
setTodos(newTodos);
}
And this doesnt
const handleToggle = (id) => {
setTodos(prevTodos => prevTodos.map(todo => {
if (todo.id === id) {
todo.completed = !todo.completed
}
}))
}
Why do i have to create a copy of the old todos array if i want to change some item inside it?
You are doing a copy of the array in both cases, and in both cases you are mutating the state directly which should be avoided. In the second version you also forgot to actually return the todo, so you will get an array of undefined.
Instead you should shallow copy the todo you want to update.
const handleToggle = (id) => {
setTodos(prevTodos => prevTodos.map(todo => {
if (todo.id === id) {
return {...todo, completed: !todo.completed}
}
return todo
}))
}
Why mutating state is not recommanded? source
Debugging: If you use console.log and don’t mutate state, your past
logs won’t get clobbered by the more recent state changes. So you can
clearly see how state has changed between renders.
Optimizations: Common React optimization strategies rely on skipping
work if previous props or state are the same as the next ones. If you
never mutate state, it is very fast to check whether there were any
changes. If prevObj === obj, you can be sure that nothing could have
changed inside of it.
New Features: The new React features we’re
building rely on state being treated like a snapshot. If you’re
mutating past versions of state, that may prevent you from using the
new features.
Requirement Changes: Some application features, like implementing
Undo/Redo, showing a history of changes, or letting the user reset a
form to earlier values, are easier to do when nothing is mutated. This
is because you can keep past copies of state in memory, and reuse them
when appropriate. If you start with a mutative approach, features like
this can be difficult to add later on.
Simpler Implementation: Because
React does not rely on mutation, it does not need to do anything
special with your objects. It does not need to hijack their
properties, always wrap them into Proxies, or do other work at
initialization as many “reactive” solutions do. This is also why React
lets you put any object into state—no matter how large—without
additional performance or correctness pitfalls.
In practice, you can often “get away” with mutating state in React,
but we strongly advise you not to do that so that you can use new
React features developed with this approach in mind. Future
contributors and perhaps even your future self will thank you!
when you're changing an object using a hook correctly (- in this case useState) you are causing a re-render of the component.
if you are changing the object directly through direct access to the value itself and not the setter function - the component will not re-render and it will likely cause a bug.
and the .map(function(...)=>{..}) is a supposed to return an array by the items that you are returning from the function within. since you're not returning anything in the second example - each item of the array will be undefined - hence you'll have an array of the same length and all the items within will be undefined.
these kinds of bugs will not happen if you remember how to use array functions and react hooks correctly,
it's usually really small things that will make you waste hours on end,
I'd really recommend reading the documentation.
I've implemented a fairly simple means to add undo to useReducer with the following:
export const stateMiddleware = reducerFunc => {
return function(state = { history: [] }, action) {
return {
history:
action.type === UNDO
? drop(state.history) // lodash drop
: [reducerFunc(state.history[0], action), ...state.history],
}
}
}
and
const [state, dispatch] = useReducer(stateMiddleware(reducer), { history: [initialState] })
The state variable consists of the entire history so present state will have to be extracted from the first element.
I'm wondering whether useReducer will accept an array for the state parameter (and also welcome to comments on my approach - it avoids using what seem to be hefty packages to do something similar)
If all you are really asking is if you can initialize useReducer hook state with a plain array, then yes, you can.
To give you some feedback on your general approach, I'll quote the Redux docs, as they provide many best practices about global state management. It makes switching easier, if you need a full blown store later on. I'll assume here that you want use the useReducer not just for one local component, but a more global state tree (thanks #DrewReese for the comment on this). If it's only about one component or so, your approach is perfectly fine!
Redux docs concerning basic state shape and wether an array is possible at the top level (Link):
A Redux state usually has a plain Javascript object as the top of the state tree. (It is certainly possible to have another type of data instead, such as a single number, an array, or a specialized data structure, but most libraries assume that the top-level value is a plain object.) The most common way to organize data within that top-level object is to further divide data into sub-trees, where each top-level key represents some "domain" or "slice" of related data.
So basically, its OK, but other libraries could be possibly opiniated and expect an object, as it is by far the most likely choice.
If you implement this shape, be sure that your requirement is to always need an undo action for your whole state. Because it comes with a cost (besides mentioned 3rd party lib compatibility):
adds one additional level of nesting directly at the top level
scalability and granularity (see next point)
What, if your app gets really huge and you have hundreds of reducers. Each time, your action is dispatched, there will be a new state object put in the history array, which could get messy.
What, if you don't want a global undo, but a very fine granular one (in one particular reducer, say for one particular form element in the UI). You would have to distinguish between a global undo and a specific undo then. What instead about just one "UNDO" action which can be handled in each interested reducer in its own manner?
If that still fits your requirements, give it a try !
Cheers
It is possible, but as ford04 pointed out it may not be a great idea for a variety of reasons.
The demo below shows it works to use a plain array as the state in useReducer.
const initialState = [];
function reducer(state, action) {
switch (action.type) {
case 'increment':
return [...state, state.length + 1];
case 'decrement':
return [...state.slice(0, -1)];
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = React.useReducer(reducer, initialState);
return (
<React.Fragment>
<div>
State: {state.join()}
</div>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
</React.Fragment>
);
}
ReactDOM.render(<Counter />, document.getElementById('app'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
<div id="app"></div>
I'm a newbie. I'm not sure, is using below the pattern wrong/silly?
import { createAction, handleActions } from "redux-actions";
const CHANGE_STATE = "appState/CHANGE_STATE";
export const changeState = createAction(CHANGE_STATE, (key, val) => ({ [key]: val }));
const initialState = {
maxBodySize: 1920,
isMaxBody: true,
isMobile: false
};
export default handleActions(
{
[CHANGE_STATE]: (state, { payload: changedState }) => {
const newState = {
...state,
...changedState
};
return newState;
}
},
initialState
);
Only one actionCreator editable every state. Like this:
// Can editable every state with 'changeState' action.
appState.changeState("isMaxBody", true);
appState.changeState("isMobile", true);
appState.changeState("maxBodySize", 960);
Can I continue to use this handy pattern?
If this pattern is bad, give some instructions to me, please.
Can I continue to use this handy pattern
I don't see why you "couldn't". It seems perfectly valid, in a strict sense.
As for whether or not it's a good idea, well, that's open to opinion, but I would not recommend using this pattern. Here's a few reasons:
You lose the abstraction between "actions" and "state modification". It's nice to be able to say "perform this action", and not have to worry about what the actual state changes are. Some actions may modify multiple parts of state. All of that is abstracted away from you, making it more controlled and testable. In a similar vein, this probably won't work well with, say, middleware, for similar reasons.
Modifying string properties "looks icky", and generally seems error prone. You'll get no help from tooling if you accidentally put a typo in a property name somewhere in your app.
Similar to the above, you lose any ability to leverage static typing (flow or TypeScript, for instance). Having written a decently large React/Redux app in a team, I highly recommend using something like TypeScript, and this pattern will not work well in a strongly/statically typed app.
Those are my opinions, derived from my experience. If the pattern works well for you and what you are trying to accomplish, you might find that it's just fine.
P.S. if you are going to use a pattern like this, I don't see why you wouldn't just do e.g. appState.changeState({isMaxBody: true}); instead of appState.changeState("isMaxBody", true);. It seems a little cleaner, and at least closer to allowing better typing.
I'm working on a React+Redux app that must save certain values from the store to localStorage. I have some utility function, that securely store values to localStorage. I have an action (thunk) and reducer.
const wallet = (state = getFromLocalStorage('wallet'), action) => {
switch(action.type) {
case 'SET_WALLET':
setToLocalStorage('wallet', action.payload); // <--- **SIDE EFFECT?**
retrun action.payload;
default:
return state;
}
}
Reducers must be pure functions. Does the getToLocalStorage function call break the purity of the reducer? And if so, where is the proper place to call this function?
As the other comment stated: yes, it's definitely a side effect. Now, it's probably not going to break anything in this case, but a reducer isn't the right place for that.
A better solution would be to either do this in a store subscription callback or a middleware. Or, even better, use one of the many store persistence libraries already available. Doing so in a thunk would also be an acceptable option, especially if you just want to persist a small amount of data rather than the entire store state.
It's definitely a side effect in FP terms, but actually there is nothing bad with your code. There is no other better workaround of sharing your state between components (or even outside react.js tree) except redux/flux maybe.
Functional programming is a good thing but you don't need to turn that into cult
Why should objects in Redux be immutable?
I know that some frameworks such as Angular2 will use onPush and can take advantage of immutability to compare states of views for faster rendering, but I am wondering if there are other reasons as Redux is framework agnostic and yet it mentions within its own docs to use immutability (regardless of the framework).
Appreciate any feedback.
Redux is a small library that represents state as (immutable) objects. And new states by passing the current state through pure functions to create an entirely new object/application states.
If your eyes-glazed over there don't worry. To sum up, Redux does not represent changes in your application's state by modifying objects ( as you would with object-oriented paradigms). Instead state changes are represented as the difference between the input object and the output object (var output = reducer(input)). If you mutate either input or output you invalidate the state.
To sum up another way, immutability is a requirement of Redux because Redux represents your application state as "frozen object snapshots". With these discrete snapshots, you can save your state, or reverse state, and generally have more "accounting" for all state changes.
State of your app is only changed by a category of pure functions called reducers. Reducers have 2 important properties:
They never mutate, returning newly built objects: This allows reasoning about input + output without side-effects
Their signature is always function name(state, action) {}, so it makes it easy to compose them:
Assume the state looks like this:
var theState = {
_2ndLevel: {
count: 0
}
}
We want to increment the count, so we make these reducers
const INCR_2ND_LEVEL_COUNT = 'incr2NdLevelCount';
function _2ndlevel (state, action) {
switch (action.type) {
case INCR_2ND_LEVEL_COUNT:
var newState = Objectd.assign({}, state);
newState.count++
return newState;
}
}
function topLevel (state, action) {
switch (action.type) {
case INCR_2ND_LEVEL_COUNT:
return Object.assign(
{},
{_2ndLevel: _2ndlevel(state._2ndlevel, action)}
);
}
}
Note the use of Object.assign({}, ...) to create an entirely new objects in each reducer:
Assuming we have wired up Redux to these reducers, then if we use Redux's event system to trigger a state change ...
dispatch({type: INCR_2ND_LEVEL_COUNT})
...Redux will call:
theNewState = topLevel(theState, action);
NOTE: action is from dispatch()
Now theNewState is an entirely new object.
Note: You can enforce immutability with a library (or new language features), or just be careful to not mutate anything :D
For a deeper look, I highly recommend you checkout this video by Dan Abramov (the creator). It should answer any lingering questions you have.
The following benefits of immutability are mentioned in Redux documentation:
Both Redux and React-Redux employ shallow equality checking. In particular:
Redux's combineReducers utility shallowly checks for reference changes caused by the reducers that it calls.
React-Redux's connect method generates components that shallowly check reference changes to the root state, and the return values from the mapStateToProps function to see if the wrapped components actually need to re-render. Such shallow checking requires immutability to function correctly.
Immutable data management ultimately makes data handling safer.
Time-travel debugging requires that reducers be pure functions with no side effects, so that you can correctly jump between different states.
The main reason Redux is using immutability is that it doesn't have to traverse an object tree to check for the changes in every key value. Instead, it will only check the object's reference is changed or not in order to update DOM on state change.
Greate article https://medium.cobeisfresh.com/how-redux-can-make-you-a-better-developer-30a094d5e3ec
Along with immutable data, pure functions are one of the core concepts
of functional programming
Based on the official docs :
There are several reasons why you must not mutate state in Redux:
It causes bugs, such as the UI not updating properly to show the latest values
It makes it harder to understand why and how the state has been updated
It makes it harder to write tests
It breaks the ability to use "time-travel debugging" correctly
It goes against the intended spirit and usage patterns for Redux
Reudx official docs