A react `useReducer` with some dependencies - reactjs

i'm creating a react component: TxtEditor
Inside the editor, there is a useReducer hook for manipulating the text:
APPEND => to alter the current text.
UPPERCASE => to convert uppercase letter.
But the reducer function is not a pure function. There are some dependencies:
props.disabled => ignores all the reducer actions.
props.onTxtChanged => will be executed after the text has modified.
So I created the reducer function inside the function component.
This is a problem for useReducer because each time the component rendered, a new identical function with different by reference always re-created.
Thus making useReducer executing the reducer function twice on next render -and- triggering props.onTxtChanged twice too. I don't know why react doing this.
Then to solve the problem, I wrapped the reducer function with useCallback.
It seem be working, but NOT. Because the props.onTxtChanged might be passed by user with an inline function. And the function always be re-created (identical but different by reference), thus making useCallback useless.
And finally I created a reducer function outside the function component.
The function is always the same by reference and making useReducer working properly.
To inject the dependencies I made a HACK like this:
const [state, dispatch] = useReducer(txtReducer, /*initialState: */{
text : 'hello',
props : props, // HACK: a dependency is injected here
});
state.props = props; // HACK: a dependency is updated here
So the props can be accessed in the reducer function:
const txtReducer = (state, action) => {
const props = state.props;
if (props.disabled) return state; // disabled => no change
}
It's working but it contain a hack.
I want the professional way doing this stuff.
Do you have any suggestion?
See the complete sandbox code here

This is what I would do:
function TxtEditor(props) {
const [state, dispatch] = useReducer(txtReducer, { text : 'hello'});
// Wait for the state to change and only then emmit a text change
useEffect(() => {
props.onTextChange(state.text);
}, [state])
return <input onInput={handleInput} />
function handleInput() {
if (props.disabled) return; // <-- just don't fire an update
// [...] call to your reducer
}
}
Reducers are just a useState with a bit of logic. So only let them handle a (singular) state and don't make it responsible for many things at once. Also they should only be responsible for the actual state logic, not something outside, like if the text box is disabled or not.
A potential solution to the desired hook described in the comments of this post:
I hope you don't mind the typescript. I just find it easier to work with.
enum TextProcessorMode {
APPEND,
}
interface TextProcessorActionOptions {
disabled?: boolean;
mode: TextProcessorMode;
}
interface TextProcesserAction {
newText: string;
options: TextProcessorActionOptions;
/**
* Is called when the text was successfully processed.
*/
onChange(text: string): void;
}
export default function textProcessorReducer(
state: string,
action: TextProcesserAction
): string {
if (action.options.disabled) return state;
let newState: string;
switch (action.options.mode) {
case TextProcessorMode.APPEND:
newState = state + action.newText;
break;
// Handle other modes
}
action.onChange(newState);
return newState;
}

Related

React updating a components state from another component causes infinite loop

I have what is basically a form wizard with multiple steps. The wizard is divided into two parts: labels and contents. When a content component changes its internal state, from say incomplete to error or something, I want the wizard to update the label. The issue I am running into is grabbing the state from the contents component, trying to save that in the wizard component as a state so I can update the labels is causing an infinite loop.
While I understand the problem I don't really know how to solve it. This is a very minimal example and my real components use some advanced features like cloneElement to pass props to user components without them having to worry about setting 10 different props. So far this has worked flawlessly until now.
So I understand that each time I update my main components state, it's going to re-render the children, which will call the same set state function forever. What can I do instead?
import React from "react";
import "./styles.css";
// This component containsLabel children
interface LabelState {
error: boolean;
}
interface LabelGroupProps {
states: LabelState[],
}
const LabelGroup = (props: LabelGroupProps) => {
return (<div>I am Comp A</div>)
}
// This component contains Component children
interface ContentState {
error: boolean;
}
interface ContentGroupProps {
getStates: (state: ContentState, index: number) => void
}
const ContentGroup = (props: ContentGroupProps) => {
// Indicate that step 2 has an error
props.getStates({
error: true
}, 2)
return (<div>I am Comp B</div>)
}
// This is the main wizard that contains both above components
// When the state of the a content component changes, the labels must be updated.
const App = () => {
const [states, setStates] = React.useState<LabelState[]>([]);
const getStates = (state: ContentState, index: number) => {
// This causes an infinite loop
// The intention is to save this state and then update the labels
const temp = [];
temp[index] = state;
setStates(prev => [...prev, ...temp])
}
return (
<div className="App">
<LabelGroup states={states}/>
<ContentGroup getStates={getStates}/>
</div>
);
};
export default App;
https://codesandbox.io/s/react-fiddle-forked-1ypi6
Your issue is caused by updating state every time ContentGroup renders. Any time there is a change in props or state, a component will automatically re-render. Then the component updates state again, then we render again, and so on. We can fix that.
What is your intention with calling getStates? You say:
// Indicate that step 2 has an error
An error could be in response to some event. For example, the user has clicked 'submit' on a form and you have a custom validation error. That might look like this:
const ContentGroup = (props: ContentGroupProps) => {
// Indicate that step 2 has an error
return (
<form
onSubmit={(event) => {
event.preventDefault();
const someErrorCondition = /* snip */;
props.getStates({
error: someErrorCondition
}, 2);
}}
>
{/* snip */}
</form>
)
}
This would not cause an infinite loop in the render because a change in state does not cause ContentGroup to call the onSubmit handler again.
Sometimes you want to perform initialization and cleanup as a component mounts and unmounts:
const ContentGroup = (props: ContentGroupProps) => {
// Indicate that step 2 has an error
React.useEffect(() => {
props.getStates({
error: true
}, 2);
return function cleanup() {
props.getStates({
error: false
}, 2);
};
}, []);
return (
<form
onSubmit={(event) => {
event.preventDefault();
/* snip */
}}
>
{/* snip */}
</form>
)
}
This does not cause an infinite loop because getStates will only be called twice: when the component mounts and right before the component unmounts.
Please treat this as pseudo-code because there are more details to leveraging hooks. Here's the documentation for useEffect, https://reactjs.org/docs/hooks-reference.html#useeffect The example code I posted won't pass many linters because it does not declare getStates as part of the hook's dependency array. In this one case, it's actually fine because we're only going to run the effect once. Further changes to getStates will be ignored. Here are more details about the dependency array of a hook: https://reactjs.org/docs/hooks-reference.html#conditionally-firing-an-effect

Problem with filling input and communication problem with redux in React

There is a base that contains text from inputs:
let data={
textInput:"",
inputInfo:{size:""},
};
export const loginReducer=(state=data,action)=>{
switch (action.type) {
case "ChangeInputInfoSize":
if (Number(action.size)|| action.size==="" ){
let Statecopy={...state};
Statecopy.inputInfo.size=action.size;
return {...Statecopy};
}else {
return state
}
default:
return state;
}
}
export const ChangeInputInfoSizeAC=(size)=>({
type:"ChangeInputInfoSize",
size
});
As well as the component and its container:
import React from "react"
import {connect} from "react-redux";
import {DataFilling} from "./DataFilling";
import {ChangeInputInfoSizeAC} from "../store/loginReducer";
let MapStateToProps=(state)=>{
return {
inputInfo:state.loginReducer.inputInfo,
textInput:state.loginReducer.textInput
}
};
export const DataFillingContainer=connect(MapStateToProps,{ChangeInputInfoSizeAC})(DataFilling)
Component:
import React from "react"
export let DataFilling = (props) => {
let ChangeInputInfoSizeFunc = (e) => {
props.ChangeInputInfoSizeAC(e.target.value)
};
<input placeholder="size" value={props.inputInfo.size} onChange={ChangeInputInfoSizeFunc} />
}
When you try to fill the field, there is no change in the field, but if you replace inputInfo.size with textInput everywhere, then everything will work.
What needs to be changed in the first option for it to work?
I guess problem is in reducer where creating Statecopy
let Statecopy={...state};
Statecopy.inputInfo.size=action.size;
In first line let Statecopy={...state} will create a new object {textInput, inputInfo} but in second line Statecopy.inputInfo actually referencing to the old inputInfo, so it overwrite size in old inputInfo and keep its reference unchanged.
I recommend make states flat as far as possible or make sure creating new state.
let Statecopy={...state,inputInfo:{size:action.size}};
In addition, you can do few improvement to your code by following these steps:
Camel case naming is Javascript convention, so instead of let Statecopy write let stateCopy
Dispatch type convention is uppercase separated with underlines, so instead of "ChangeInputInfoSize" write "CHANGE_INPUT_INFO_SIZE"
When declaring a function use const because you don't want accidentally overwrite a function, so const ChangeInputInfoSizeFunc = (e) => {... is correct. This is true for export const DataFilling as well.
For a controlled component like DataFilling only dispatch the final state, so onChange event will save value in a local state, then an onBlur event dispatch the result to reducer.
Limit declaring new variables as far as possible, so in reducer you don't need stateCopy,
Make reducer initial state a const variable, you don't want overwrite it.
in if (Number(action.size)|| action.size==="" ) first part will be a number but second part will be an empty string. Because of consistency and preventing future bugs make both same. Also you can do this validation before dispatching size to the reducer.
Hopefully, all these recommendations will help to avoid future bugs
I had to do deep copy
case "ChangeInputInfoSize":{
if (Number(action.size)|| action.size==="" ){
let copyState = {...state};
copyState.inputInfo = {...state.inputInfo};
copyState.inputInfo.size=action.size
return {...copyState};
}else {
return state
}
}

useState vs useReducer

useReducer is usually preferable to useState when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one. useReducer also lets you optimize performance for components that trigger deep updates because you can pass dispatch down instead of callbacks.
(quote from https://reactjs.org/docs/hooks-reference.html#usereducer)
I'm interested in the bold part, which states that useReducer should be used instead of useState when being used in contexts.
I tried both variants, but they don't appear to differ.
The way I compared both approaches was as follows:
const [state, updateState] = useState();
const [reducerState, dispatch] = useReducer(myReducerFunction);
I passed each of them once to a context object, which was being consumed in a deeper child (I just ran separate tests, replacing the value by the function that I wanted to test).
<ContextObject.Provider value={updateState // dispatch}>
The child contained these functions
const updateFunction = useContext(ContextObject);
useEffect(
() => {
console.log('effect triggered');
console.log(updateFunction);
},
[updateFunction]
);
In both cases, when the parent rerendered (because of another local state change), the effect never ran, indicating that the update function isn't changed between renders.
Am I reading the bold sentence in the quote wrong? Or is there something I'm overlooking?
useReducer also lets you optimize performance for components that
trigger deep updates because you can pass dispatch down instead of
callbacks.
The above statement is not trying to indicate that the setter returned by useState is being created newly on each update or render. What it means is that when you have a complex logic to update state you simply won't use the setter directly to update state, instead you will write a complex function which in turn would call the setter with updated state something like
const handleStateChange = () => {
// lots of logic to derive updated state
updateState(newState);
}
ContextObject.Provider value={{state, handleStateChange}}>
Now in the above case everytime the parent is re-rendered a new instance of handleStateChange is created causing the Context Consumer to also re-render.
A solution to the above case is to use useCallback and memoize the state updater method and use it. However for this you would need to take care of closure issues associated with using the values within the method.
Hence it is recommended to use useReducer which returns a dispatch method that doesn't change between re-renders and you can have the manipulation logic in the reducers.
Practical observation on useReducer and useState -
UseState:
In my React Native project I've 1 screen containing 25+ different states created using useState.
I'm calling an api in useEffect (componentDidMount) and on getting the response based on some conditions, I'm setting up these 25 states, calling 25 state setter function for each function.
I've put a re-rendering counter and checked my screen is re-rendered 14 times.
re-rendering count likewise :
let count = 0;
export default function Home(props) {
count++;
console.log({count});
//...
// Rest of the code
}
UseReducer :
Then I've moved these 25 states in useReducer states, And used only single action to update these states on API response.
I've observed there is only 2 re-render.
//API calling method:
fetchData()
{
const response = await AuthAxios.getHomeData();
dispatch({type: 'SET_HOME_DATA', data: response.data});
}
//useReducer Code:
const initialStaes = {
state1: null,
state2: null,
//.....More States
state27: null,
state28: null
}
const HomeReducer = (state, action) => {
switch (action.type) {
case 'SET_HOME_DATA': {
return {
...state,
state1: (Data based on conditions),
state2: !(some Conditions ),
//....More states
state27: false
}
}
}
}
Advantage of useReducer in this case :
Using useReducer I've reduced number of re-renders on the screen, hence better performance and smoothness of the App.
Number of lines is reduced in my screen itself. It improved code readablity.
When you need to care about it
If you create a callback on render and pass it to a child component, the props of that child will change. However, when the parent renders, a regular component will rerender (to the virtual dom), even props remain the same. The exception is a classComponent that implements shouldComponentUpdate, and compares props (such as a PureComponent).
This is an optimization, and you should only care about it if rerendering the child component requires significant computation (If you render it to the same screen multiple times, or if it will require a deep or significant rerender).
If this is the case, you should make sure:
Your child is a class component that extends PureComponent
Avoid passing a newly created function as a prop. Instead, pass
dispatch, the setter returned from React.useState or a memoized
customized setter.
Using a memoized customized setter
While I would not recommend building a unique memoized setter for a specific component (there are a few things you need to look out for), you could use a general hook that takes care of implementation for you.
Here is an example of a useObjState hook, which provides an easy API, and which will not cause additional rerenders.
const useObjState = initialObj => {
const [obj, setObj] = React.useState(initialObj);
const memoizedSetObj = React.useMemo(() => {
const helper = {};
Object.keys(initialObj).forEach(key => {
helper[key] = newVal =>
setObj(prevObj => ({ ...prevObj, [key]: newVal }));
});
return helper;
}, []);
return [obj, memoizedSetObj];
};
function App() {
const [user, memoizedSetUser] = useObjState({
id: 1,
name: "ed",
age: null,
});
return (
<NameComp
setter={memoizedSetUser.name}
name={user.name}
/>
);
}
const NameComp = ({name, setter}) => (
<div>
<h1>{name}</h1>
<input
value={name}
onChange={e => setter(e.target.value)}
/>
</div>
)
Demo

immutable react reducer state is not updating

I am trying to create a simple website using react-redux and the immutable-assign library (instead of immutable) to handle my state. (documentation for immutable-assign: https://github.com/engineforce/ImmutableAssign)
I've made solutions with both the 'immutable' and 'immutable-assign' libraries, but neither work (code for immutable solution is commented out in the reducer below. No matter which changes I make, the state never changes, and the values are never assigned to menuItems
The setMenu(newMenu) function is currently called with dummydata in the form of a list of arrays in the following format:
menuItems: {
id: "113",
foodItem: "tesatewr",
description: "gfdgsdfsdf",
price: 999
}
The reducer:
import { iassign } from 'immutable-assign'
export function setMenu(newMenu) {return {type: 'SET_MENU_ITEMS', newMenu}}
const initialState = {
date: 'test',
menuId: 'test',
menuItems: []
}
function menuViewReducer(state = initialState, action){
switch(action.type){
case 'SET_MENU_ITEMS':
var itemList = iassign(
state,
function (n) { n.push('testtest'); return n}
)
return state.set(['menuItems'], itemList)
default:
return state
}
}
/* CODE FOR IMMUTABLE
function menuViewReducer(state = fromJS(initialState), action){
switch(action.type){
case 'SET_MENU_ITEMS':
return state.updateIn(['menuItems'], (menuItems) => menuItems.push(fromJS(action.newMenu.menuItems)))
default:
return state
}
} */
export const menuSelector = {
date: state => state.menuViewList.date,
menuId: state => state.menuViewList.menuId,
menuItems: state => state.menuViewList.menuItems
}
export default menuViewReducer
Render function:
render(){
return (
<div>
Test data here: {this.props.menuItems}
<ul className="menuViewList">{ this.mapMenuItemsToListElements() }</ul>
<button
onClick={() => this.mapMenuItemsToListElements()}> get data
</button>
</div>
)
}
It's really hard to figure out what's not working from this code. The best I can do is give you some debugging tips:
First off, are you getting any errors? If yes, that seems like a good place to start.
Otherwise, try to narrow down where the problem is occurring.
Are you sure your reducer is actually getting called?
I would try putting a console.log right after your case 'SET_MENU_ITEMS': so you know when your code is being run.
If it's not:
The problem could be a number of things:
Your reducer isn't connected to your store properly
You're not properly dispatching actions to your store
The actions you're dispatching don't have their type property properly set.
If it is:
The problem could be a number of different things. Some that I can think of:
Your state isn't being updated (properly). Try logging the state at the start of your reducer and your new state right before you return it. Or consider using redux-devtools to inspect your state.
Your view isn't getting updated. Maybe your component isn't connected to your store properly.
I found the error and as Simon pointed out, its hard to find from my submitted code.
I was calling setMenu(newMenu) in a generator function, so I should have called it like this:
yield put(setMenu(newMenu))
instead of
setMenu(newMenu)

how should I build onClick action in my react component + Redux

I've been through many tutorials and questions on Stack but I can't find a solution. I'm just learning React/redux, trying to build OnClick action. I've got the following error "Maximum call stack size exceeded error". I got this because I'm rendering a function that's changing my state infinitely. I'm trying to deal with my <button onClick={DisplayTable(click)}>cool</button> differently but nothing seems to work.
I also know that my action and I guess my reducers works properly since when I'm dispatching my action trough the console : $r.store.dispatch({type: 'SET_TABLE_DATA'});, my state is updated properly.
Any advices ?
here is my action :
export const setTableFilter = (click) => {
return {
type: 'SET_TABLE_DATA',
click : click,
};
};
here is my reducer :
const tableFilter = (state = 0, action) => {
if(action.type === 'SET_TABLE_DATA') {
return state + 1;
}
return state;
}
and here is my component :
const DisplayTable = (click) => {
return (
<div>
<button onClick={DisplayTable(click)}>cool</button>
</div> )
}
function mapStateToProps(state) {
return {
click: state.tableFilter.click
};
};
const mapDispachToProps = (dispatch) => {
return {
DisplayTable: (click) => {dispatch (setTableFilter(click));
},
};
};
const AppTable = connect(mapStateToProps, mapDispachToProps)(DisplayTable);
export default AppTable;
I also know that I should build my reducer in a way that my state should be updated without any mutation, however I'll keep this for later ! :)
thanks.
The answer given doesn't really explain why your code was not working, so I thought I'd expand on that.
Your problem is that you are exceeding the function call stack, more commonly known as infinite recursion. The reason this is happening is because you aren't passing a function to the onClick attribute of your button, but rather invoking a function and passing its return value instead. So the following scenario is happening:
React component is mounted to the DOM
render() is called
The DisplayTable function is invoked, which dispatches an update to the store
The store updates, and passes new props to the React component
render() is called again
DisplayTable is invoked again
...and so on.
What you'll want to do instead is pass the function to the button's onClick attribute. So your component should look like this:
const Component = props => {
return (
<div>
<button onClick={props.DisplayTable}>cool</button>
</div>
);
};
In that above code snippet, I removed your click prop because it doesn't look like you're using it at all (given the code you posted in the OP).
A few tips, not a complete solution since that would not help you learn:
Your action and reducer are looking fine. You are passing the click property which is not used in the reducer. Maybe you will use it in the future but for now it is useless.
A React component function takes props as an argument:
const Comp = props => {
const click = props.click;
// ...
};
mapDispatchToProps is usually not needed. Use plain objects instead:
connect(state => state.tableFilter, { setTableFilter })(DisplayTable);
You can then access the function from props:
<button onClick={() => props.setTableFilter(click)}>cool</button>
Keep in mind: onClick takes a function!
Also the state you defined in the reducer has no property called click, instead it is a number (see correct mapStateToProps function above)

Resources