How to avoid useless rerender using useReducer([state,dispatch]) and useContext? - reactjs

When using multiple useReducers every component using a part of the state rerenders.
import React, { useContext } from 'react'
import Store from '../store'
import { setName } from "../actions/nameActions"
const Name = () => {
const { state: { nameReducer: { name } }, dispatch } = useContext(Store)
const handleInput = ({ target: { value } }) => { dispatch(setName(value)) }
console.log('useless rerender if other part (not name) of state is changed');
return <div>
<p>{name}</p>
<input value={name} onChange={handleInput} />
</div>
}
export default Name;
How to avoid this useless rerendering?

If useState or useReducer state changes, the component is updated, there is no way to prevent this in the component itself.
Re-render should be prevented in child component that depends on partial state, e.g. by making it pure:
const NameContainer = () => {
const { state: { nameReducer: { name } }, dispatch } = useContext(Store)
return <Name name={name} dispatch={dispatch}/>;
}
const Name = React.memo(({ name, dispatch }) => {
const handleInput = ({ target: { value } }) => { dispatch(setName(value)) }
return <div>
<p>{name}</p>
<input value={name} onChange={handleInput} />
</div>
});
NameContainer can be rewritten to a HOC and serve the same purpose as Redux connect, to extract needed properties from a store and map them to connected component props.

Related

How to test a controlled input (via Redux state) that dispatches an action on its onChange handler?

This is my component:
import React, { ChangeEvent } from "react";
import { Input } from "#chakra-ui/react"
import { useDispatch, useSelector } from "react-redux";
import { ACTION } from "../../redux/appSlice";
import { RootState } from "../../redux/store";
interface IAddTodoForm {}
const AddTodoForm: React.FC<IAddTodoForm> = (props) => {
const dispatch = useDispatch();
const { addTodoForm } = useSelector((state: RootState) => state.app);
const { title } = addTodoForm;
const onChange = (e: ChangeEvent<HTMLInputElement>) => {
const title = e.target.value;
dispatch(ACTION.UPDATE_TODO_INPUT({title}));
};
return(
<Input
value={title}
onChange={onChange}
/>
);
};
export default React.memo(AddTodoForm);
It's a basic input that dispatches to Redux from the onChange handler.
This is what I'd like to test:
import { render, screen, fireEvent } from "../../../test/test-utils";
import AddTodoForm from "../AddTodoForm";
beforeEach(() => {
render(<AddTodoForm/>); // NOTE: THIS IS A CUSTOM render FUNCTION THAT ALREADY WRAPPED WITH THE <Provider store={store}> FROM react-redux
});
test("AddTodoForm updated input vale", () => {
const { container } = render(<AddTodoForm/>);
const input = container.getElementsByTagName("input")[0];
expect(input).toBeInTheDocument();
fireEvent.change(input, { target: { value: "NEW TODO" }});
// HERE I WOULD LIKE TO CHECK IF THE INPUT VALUE HAS BEEN UPDATE
// HOW CAN I DO THAT
});
As you can see, I would like to fire a change event, that should dispatch to the Redux store, and then I would like to confirm that the input has been updated with the NEW TODO value. Is this the correct approach?
You would simply use an expect like so:
test("AddTodoForm updated input vale", () => {
const input = screen.getByLabelText("add-todo-input");
expect(input).toBeInTheDocument();
fireEvent.change(input, { target: { value: "NEW TODO" }});
expect(input.value).toBe('NEW TODO')
});
For an async operation, you could use this method instead:
await screen.findByText("NEW TODO");
expect(getByText("NEW TODO")).toBeTruthy();
By using await findByText you wait for the text to appear.

Pass text value to another component

How to pass text value to another component using Redux in React?
I am learning Redux in React. I am trying to pass text value to another component using Redux in React.
My code is like below
Mycomponent.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
class Mycomponent extends Component {
state = {
textInput: '',
}
handleChange = event => {
this.props.dispatch({ type: "add" });
}
render = () => {
return (
<div>
<input
type="text"
onChange={this.handleChange} />
</div>
);
}
}
const mapStateToProps = state => ({ nameState: state.nameState});
export default connect(mapStateToProps)(Mycomponent);
nameAction.js
export const nameAction = () => ({
type: 'add'
});
export default { nameAction };
nameReducer.js
const nameReducer = (state = {}, action) => {
switch (action.type) {
case 'add': {
return {
...state,
nameState: action.payload
};
}
default:
return state;
}
};
export default nameReducer;
Outputcomponent.js
import React, { Component } from 'react';
class Outputcomponent extends Component {
render = (props) => {
return (
<div>
<div>{this.props.nameState }</div>
</div>
);
}
}
export default Outputcomponent;
The use of redux hooks explained by Josiah is for me the best approach but you can also use mapDispatchToProps.
Even if the main problem is that you don't pass any data in your 'add' action.
nameAction.js
You call the action.payload in nameReducer.js but it does not appear in your action
export const nameAction = (text) => ({
type: 'add',
payload: text
});
Mycomponent.js
Then as for your state we can mapDispatchToProps.
(I think it's better to trigger the action with a submit button and save the input change in your textInput state, but I guess it's intentional that there is none)
import React, { Component } from 'react';
import { connect } from 'react-redux';
import {nameAction} from './nameAction'
class Mycomponent extends Component {
state = {
textInput: '',
}
handleChange = event => {
this.props.nameAction(event.target.value);
}
render = () => {
return (
<div>
<input
type="text"
onChange={this.handleChange} />
</div>
);
}
}
const mapStateToProps = state => ({ nameState: state.nameState});
const mapDispatchToProps = dispatch => ({ nameAction: (text) => dispatch(nameAction(text))});
export default connect(mapStateToProps,mapDispatchToProps)(Mycomponent);
OutputComponent.js
to get the data two possibilities either with a class using connect and mapStateToProps , or using the useSelector hook with a functional component.
with a Class
import React, { Component } from "react";
import { connect } from "react-redux";
class OutputComponent extends Component {
render = () => {
return (
<div>
<div>{this.props.nameState}</div>
</div>
);
};
}
const mapStateToProps = state => state;
export default connect(mapStateToProps)(OutputComponent);
with a functional component
import React from "react";
import { useSelector } from "react-redux";
const OutputComponent = () => {
const nameState = useSelector((state) => state.nameState);
return (
<div>
<div>{nameState}</div>
</div>
);
};
export default OutputComponent;
Of course you must not forget to create a strore and to provide it to the highest component
store.js
import { createStore } from "redux";
import nameReducer from "./nameReducer";
const store = createStore(nameReducer);
export default store;
index.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { Provider } from "react-redux";
import store from "./store";
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);
Component
const AddTodo = () => {
const [todo, setTodo] = useState("");
const dispatch = useDispatch();
const handleChange = (e) => setTodo(e.target.value);
const handleSubmit = (e) => {
e.preventDefault();
dispatch(addTodoAction(todo));
}
return {
<form onSubmit={handleSubmit}>
<input type="text" onChange={handleChange} />
</form>
}
)
Actions
const addTodoAction = (text) => {
dispatch({
type: "ADD_TODO",
payload: text
})
}
Reducers
const addTodoReducer = (state, action) => {
switch(action.type) {
case "ADD_TODO":
return {
todo: action.payload,
}
default:
return state;
}
}
store
// some code for store.js
Accessing this todo from another component
const ComponentA = () => {
const {todo} = useSelector(state => state.todo);
return (
<p> {todo} </p>
)
}
Side Note:
Redux comes with too much boilerplate if you want to pass text from one component to another, just use props

Dispatching components via react-redux

I want to pass a component and display it inside another one through redux.
I am doing something like this:
ComponentToDispatch.js
const ComponentToDispatch = (props) => {
return (<div>Propagate me {props.anthem}</div> {/* props.anthem = undefined, unlike what I expect.*/}
);
};
export {ComponentToDispatch};
In the following component, I have a button that dispatches the formerly defined one.
BruitEvent.js
// code above
`import {ComponentToDispatch} from "./ComponentToDispatch";
import {showComponentAction} from "./actions";
const BruitEvent =()=>{
const dispatch = useDispatch();
return (<button onClick={(evt)=>dispatch(showComponentAction(ComponentToDispatch)}>
Click to dispatch</button>);
};`
The action that triggers this event is:
actions.js
`
export function ShowComponentAction(Component) {
return {
type: SHOW_ACTION,
payload: {
component: <Component />,
},
};
}`
Finally, I can display the propagated component:
const DispayComponent = () =>{
const { component} = useSelector((state) => {
if (state.testDisplay) {
return {
component: state.testDisplay.component,
};
}
return { component: null };
});
useInjectReducer({ key: "testDisplay", reducer });
return (<div>{component}</div>);
}
export {DisplayComponent};
So far so good, thanks to David Hellsing for his insight, I can display every static thing resides in `ComponentToDispatch', but it fails to handle props.
Question: How can I transmit props while dispatching component itself?
You need to either instantiate and enclose the props before the component is dispatched, or pass the uninstantiated component and props object in the dispatched action and pass the props to the component on the receiving end. I suggest the latter, send both component and props in action.
const BruitEvent =()=>{
const dispatch = useDispatch();
return (
<button
onClick={(evt) => dispatch(
showComponentAction(ComponentToDispatch, /* some possible props object */)
)}
>
Click to dispatch
</button>
);
};
...
export function ShowComponentAction(Component, props = {}) {
return {
type: SHOW_ACTION,
payload: { Component, props }, // <-- assumes reducer places in state.testDisplay
},
};
...
const DispayComponent = () =>{
const { Component, prop } = useSelector((state) => state.testDisplay);
useInjectReducer({ key: "testDisplay", reducer });
return Component ? <div><Component {...props} /></div> : null;
}

Why is my React Input box not rendering delayed response even though the wrapper class renders the input box - React with Redux

Problem statement: The class render() is called but the underlying input box does not update field.
Description: When the app Class is loaded, I make a fetch config call to get the initial server config. Before the async calls, the input box successfully shows the initial value. However, after the async data is loaded, the class calls the render method but input value would not change.
Code:
InputBoxComponent
import React, {useState} from 'react';
export const StatefulInputBox = (props) => {
const [inputValue, changeInputValue] = useState(props.value);
console.log(inputValue);
const onChange = (e) => {
const val = e.target.value;
changeInputValue(val);
if(props.onChange) {
props.onChange(val);
}
}
return (
<input value={inputValue} onChange={(e) => {
onChange(e)
}} />
)
};
WrapperClass
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {nameActions} from "../../actions/inputBoxActions";
import {StatefulInputBox} from "./StatefulInputBox";
class StatefulNumInputBoxWrapperStoreComponent extends Component{
constructor(props) {
super();
console.log(props.inputConfig);
props.getThunkNum();
this.onValueChange = this.onValueChange.bind(this);
}
//props.getInputBoxNum();
// Without a button this goes in an infinite self rendering loop
// That is why we use a class wrapper, so that the constructor isn't called multiple times
onValueChange (newVal) {
console.log(newVal);
}
render() {
return (
<div>
<StatefulInputBox value={this.props.inputConfig.value} onChange={this.onValueChange}/>
</div>
);
}
}
const mapInputWrapperStateToProps = (state) => {
return {
inputConfig: state.inputBox
}
}
const mapInputWrapperDispatchToProps = (dispatch) => {
return {
getInputBoxNum: () => (dispatch(nameActions.getInputBoxNum())),
setInputBoxNum: (name) => (dispatch(nameActions.setInputBoxNum(name))),
getThunkNum: () => (dispatch(nameActions.getThunkNum()))
}
}
export const StatefulNumInputWrapperStoreClass = connect(mapInputWrapperStateToProps, mapInputWrapperDispatchToProps)(StatefulNumInputBoxWrapperStoreComponent);
InputBoxAction
export const nameActions = {
getInputBoxNum: () => {
return {
type: 'GET_INPUTBOX_NUM',
payload: {
loading: false,
value: 10
}
}
},
setInputBoxNum: (num) => {
return {
type: 'SET_INPUTBOX_NUM',
payload: {
loading: false,
value: num
}
}
},
getThunkNum: () => {
return (dispatch) => {
dispatch(nameActions.setLoading());
setTimeout(()=>{
dispatch(nameActions.setInputBoxNum(50));
},2000)
}
}
};
InputBoxReducer
const initialState = {
value: 'Joe',
loading: false
}
export const nameReducer = (currentState = {...initialState}, action) => {
switch (action.type) {
case 'GET_INPUTBOX_NUM': {
const newState = {...currentState, ...action.payload};
return newState;
}
case 'SET_INPUTBOX_NUM': {
const newState = {...currentState, ...action.payload};
return newState;
}
case 'NAME_SUCCESS': {
let newState = {...currentState, ...action.payload};
return newState;
}
default: {
return currentState;
}
}
}
Store
import {createStore, combineReducers, applyMiddleware} from 'redux';
import {buttonReducer} from "./components/Button/buttonReducers";
import {nameReducer} from "./reducers/inputBoxReducer";
import thunk from 'redux-thunk';
import {dropDownReducer} from "./reducers/dropDownReducer";
const rootReducer = combineReducers({
button: buttonReducer,
inputBox: nameReducer,
dropDown: dropDownReducer
});
export const store = createStore(rootReducer, applyMiddleware(thunk));
Few things to note:
All the data flow points are indicating change, meaning the constructor, the actions, the reducer, the mapStateToProps, then the class render. What is not working is the inputBox value. The console log inside the InputBox is not called after the async call.
I have used controlled form as there would be prefilled data.
I tried the wrapper with a pure component but it goes on an infinite loop as getThunkNum keeps getting called repetitively
Also suggest, what would be a better strategy to work with a default prefilled data, initial config load data, and then the normal flow of editing.
UPDATE 1
I tried the fetch API inside componentDidMount() of the wrapper class but still no luck.
UPDATE 2
I am receiving value from props but my input value is pointing to inputValue, a property of state object. So how do I decide if I should print the props or state property?
Okay, I figured out the problem.
When you have the issue of showing a default value, a pre-loaded server data (pre-fill) AND making them editable. It's best not to mix props and useState. A direct redux approach of changing the store value on every change solves the problem.
In the above problem, the inputBox looked like this:
import React, {useState} from 'react';
export const StatefulInputBox = (props) => {
const [inputValue, changeInputValue] = useState(props.value);
console.log(inputValue);
const onChange = (e) => {
const val = e.target.value;
changeInputValue(val);
if(props.onChange) {
props.onChange(val);
}
}
return (
<input value={inputValue} onChange={(e) => {
onChange(e)
}} />
)
};
Notice, the ambiguity while assigning value to the input box. If you assign props.value, then the user can't type. And if you assign value=inputValue then it can't be changed by the server call data.
I removed the useState feature and directed changes through redux itself
export const DelayedResInputBox = (props) => {
return (
<input value={props.value} onChange={(e) => {
props.onChange(e.target.value)
}} />
)
};
And in the wrapper class, I wired the updates to store using redux
class DelayedResWrapperComponent extends Component {
constructor() {
super();
this.onChange = this.onChange.bind(this);
}
onChange(val) {
console.log(val);
this.props.saveUserResponse(val)
}
componentDidMount() {
this.props.getServerData();
}
render() {
return (
<DelayedResInputBox value={this.props.delayedRes.value} onChange={this.onChange}/>
)
}
}
const mapDelayedDispatchToProps = (dispatch) => {
return {
getServerData: () => {
dispatch(delayedResActions.getDelayedThunkRes())
},
saveUserResponse: (val) => {
dispatch(delayedResActions.setValueInStore(val))
}
}
};
This way all the minor changes are routed through store and the inputBox gets only one change command. Hence, I achieved the goal by updating store via redux.
Note: Change in store does not mean submit. All smaller components will update store with their value and then a submit button will only trigger a server update. This also helps to pull out unnecessary logic in a dumb component
The only other approach would be to have three input boxes: a disabled one to show data till the config loading completes, an editable one with useState
Feel free to correct me if I am wrong.

Target not defined react

sorry for the noob question, but I am getting a target undefined. I've tried passing the componentDidMount on my onformsubmit however React is telling me the query variable is not defined.
import React, { Component } from 'react'
import DisplayData from './DisplayData';
export default class stockSearch extends Component {
state = {
searchResult: {},
}
componentDidMount = (e) => {
const query = e.target.elements.query.value
fetch(`https://min-api.cryptocompare.com/data/pricemulti?fsyms=BTC,ETH,IOT&tsyms=USD`)
.then((response) => response.json())
.then(data => {
this.setState({ searchResult: data });
console.log(this.state.searchResult);
});
}
render() {
const { searchResult } = this.state;
return (
<form onSubmit={this.props.componentDidMount}>
<label>
Name:
<input type="text" name="query" placeholder="Search Crypto" />
</label>
<button>Search Crypto</button>
<DisplayData results={searchResult} />
</form>
);
}
}
componentDidMount is one of the React Component lifecycle methods so you shouldn't pass it as the onSubmit handler. Instead, you should create a new method, e.g fetchData, which you pass to the form's onSubmit.
If you want to also fetch data on mount, you can call your handler in componentDidMount
export default class StockSearch extends Component {
state = {
searchResult: {},
queryValue: ''
}
componentDidMount() {
fetchData('default');
}
fetchData = (query) => {
fetch(`http://something.com/${query}`)
.then(...)
.then(data => {
this.setState({ searchResult: data })
});
}
render() {
return (
<form onSubmit={() => fetchData(this.state.queryValue)}>
<input
value={this.state.queryValue}
onChange={(e) => this.setState(e.target.value)}
/>
</form>
)
}
}
A few other things I've changed:
1. React components should be UpperCamelCase
2. Generally you'll manage state in your component, for example input values
.

Resources