local state values cant be equal to redux state values - javascript

hi I am using redux in the react and I have a form and the form data (specially the value of the form elements when user types something) is stored inside the local state of my react component. and at the same time I have a dispatch incrementing a counter by one and I call it when onchanged function is called on the form elements. and I show the counter data taken from redux state. so the data stored in redux is the number of keys pressed.
the issue is the value of counter cannot be entered into the form inputs. for example if i press any key (for example type a letter ) my redux counter value would be 1 and now I cant type number 1 in the inputs. the local state does not change.
here is my code:
import * as React from 'react';
import {Box} from "#material-ui/core";
import {FormElements} from "../forms/formElements";
import Typography from "#material-ui/core/Typography";
import {NavLink} from "react-router-dom";
import {connect} from "react-redux";
import ClickAwayListener from "#material-ui/core/ClickAwayListener";
class Login extends React.Component {
state = {
counter: 0,
comps: {
Lusername: {
required: true,
label: "username",
id: "Lusername",
type: "string",
value: ""
},
Lpassword: {
required: true,
type: "password",
id: "Lpassword",
label: "password",
value: ""
}
}
}
handleChange = (event) => {
this.props.onInc(); //redux dispatch
let {id, value} = event.target //local state
let comps = this.state.comps
comps[id].value = value
this.setState({comps: comps})
}
render() {
return (
<ClickAwayListener onClickAway={this.props.onclickAway}>
<Box m={2}>
<p>{this.props.ctr}</p>
<FormElements comps={this.state.comps} handleChange={this.handleChange}
handleSubmit={this.handleSubmit}></FormElements>
<Box mt={2}>
<NavLink to={"/signup"} style={{textDecoration: "none", color: "#0e404c"}}>
<Typography component={"h5"}>don't have an account? signUp</Typography>
</NavLink>
</Box>
</Box>
</ClickAwayListener>
);
};
};
const MapStateToProps = state => {
return {ctr: state.counter}
}
const MapDispatchToProps = dispatch => {
return {
onInc: () => dispatch({type: "INC"})
}
}
export default connect(MapStateToProps, MapDispatchToProps)(Login)

I see 2 problems here. First, you have a local state also called counter. it looks you are confused about redux state. React's local state is not the same as Redux's state, is a total different state. you better remove counter from your local state, there is no point if you use redux for counter state imo.
Second, these lines:
let comps = this.state.comps
comps[id].value = value
this.setState({comps: comps})
comps is an object, reference based, which means at 2nd line you are mutating state directly, which is bad practice and can lead to weird behaviors. keep in mind, comps has also nested object so a shallow destructuring like const comps = { ...this.state.comps } wont be enough. you either need to use a deepClone function from a helper lib or you do something like below to create a whole new object:
const oldComps = this.state.comps
const Lusername = { ...oldComps.Lusername }
const Lpassword = { ...oldComps.Lpassword }
const comps = { Lusername, Lpassword }
comps[id].value = value
That way you are not mutating state directly, and can manipulate it safety.
update
edit as the following:
handleChange = (event) => {
event.persist()
// ...comps logic
this.setState({comps: comps}, this.props.onInc)
}
event is react's synthetic event. It can be nullified for reuse as react docs say. it seems that's the case here.
the second change is for consistency. Increment should be triggered after comps state is updated imho. You can pass your onInc function as second argument, which will be triggered after state is updated.

Related

React/Redux component with checkboxes does not update when click on checkbox even though I'm returning new state

I've been stuck for a while trying to make the re-render in the checkboxes to work, the variables are being updated but it's just the rendering that doesn't happen.
I'm receiving a response from the backend that contains an object with an array of steps, I'm going to render a checkbox for every step if it's from a specific type. As soon as I received the object, I add in every step a new property value to use it later for checking the checkboxes.
This is my reducer:
export const MyObject = (state: MyObject = defaultState, action: FetchMyObjectAction | UpdateStepsInMyObjectAction) => {
switch (action.type) {
case "FETCH_MYOBJECT":
return {
...action.payload, // MyObject
steps: action.payload.steps.map((step) => {
if (step.control.controlType === "1") { // "1" = checkbox
return {
...step,
value: step.control.defaultValues[0] === "true" ? true : false, // Adding the property value
};
}
return step;
}),
};
case "UPDATE_STEPS":
return {
...state,
steps: state.steps.map((step) => {
if (step.id === action.payload.stepId) { // if this is the checkbox to update
return {
...step,
value: action.payload.checked,
};
}
return step;
}),
};
default:
return state;
}
This is how I'm rendering the checkboxes:
for (let step of steps) {
if (step.control.controlType === "1") {
controls.push(
<Checkbox
label={step.displayName}
checked={step.value}
onChange={(_ev, checked) => {
callback(step.id, checked);
}}
disabled={false}
className={classNames.checkbox}
/>
);
}
}
callback is a function that calls the reducer above for the case "UPDATE_STEPS".
After inspecting the variables I can see that they are being updated properly, it's just that the re-render doesn't happen in the checkboxes, not even the first time I check the box, the check doesn't appear. If I move to a different component and then go back to the component with the checkboxes I can see now the checks. But if I check/uncheck within the same component, nothing happens visually.
As far as I know, I'm returning new objects for every update, so mutability is not happening. Can you see what I'm missing?
Thanks!
First I would inspect if the checkbox works with useState to manage your state.
import { useState } from "react";
function CheckBoxForm() {
const [checked, setChecked] = useState(false);
return <Checkbox checked={checked} onChange={() => setChecked(!checked)} />;
}
Then I would check if you have wired up the reducer correctly using redux or useReducer. When you dispatch an action it should trigger a rerender. For troubleshooting this using redux please refer to the redux docs: https://react-redux.js.org/troubleshooting#my-views-aren-t-updating-when-something-changes-outside-of-redux.
You may be updating the object directly rather than dispatching an action using the function provided by the redux store. If you are using hooks, here is how you wire up your app to make sure the component props are subscribed to changing in the redux store. You must wrap with a provider, use redux hooks like useSelector and use their provided dispatch function: https://react-redux.js.org/api/hooks
Using useReducer is a much simpler process and will be easier to troubleshoot: https://beta.reactjs.org/reference/react/useReducer

React functional component not re-rendering on its own state change

So I found a hook someone made online called useStickyState and have been trying to implement it into my React app.
The issue I've come across is that when I attempt to update the state of the useStickyState hook, it doesn't actually update the local storage or re-render the component in which the state is declared. It only seems to actually update the localstorage when a different state is updated causing the component to re-render.
When I run setHelpText() from a child component, the console registers "get item" and "set item" as well as "render app container".
However, if I run setMasterText() from a child the AppContainer doesn't re-render at all.
The only difference I can think of is that setHelpText receives a string e.g. setHelpText("Blah blah blah").
Whereas setMasterText receives an updated array of objects, of which only an existing property of an array item is altered (not the number of array items).
AppContainer.js
import React, { useState } from "react";
import { useStickyState } from "../../hooks/useStickyState";
import "./AppContainer.scss";
//COMPONENTS
import HelpText from "../HelpText/HelpText";
import EditArea from "../EditArea/EditArea";
//DATA
import { HelpTextData } from "../../data/HelpTextData";
const AppContainer = () => {
const [masterText, setMasterText] = useStickyState([], "mastertext");
const [helpText, setHelpText] = useState(HelpTextData.welcome);
console.log("render app container");
return (
<div id="app-container">
<HelpText text={helpText} />
<EditArea
masterText={masterText}
setMasterText={setMasterText}
setHelpText={setHelpText}
/>
</div>
);
};
export default AppContainer;
Example of changing helpText state from a child of AppContainer:
// Previous state
helpText = "abc";
// Setting new state
setHelpText("cde");
// Re-render triggered
Example of changing masterText state from a child of AppContainer:
// Previous state
masterText = [
{
words: [
{text: "hello", selected: true},
{text: "there", selected: false}
]
},
{
words: [
{text: "Hi", selected: false},
{text: "there", selected: false}
]
},
]
// Setting new state
let newMasterText = masterText;
newMasterText[0].words[0].text = "Abc";
setMasterText(newMasterText);
// No re-render triggered
useStickyState.js
import React from "react";
export function useStickyState(defaultValue, key) {
const [value, setValue] = React.useState(() => {
const stickyValue = window.localStorage.getItem(key);
console.log("get item");
return stickyValue !== null ? JSON.parse(stickyValue) : defaultValue;
});
React.useEffect(() => {
console.log("set item");
window.localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
So I think I've figured it out!
For anyone that's having a similar issue:
I've ditched using the custom hook. Instead I am passing down a handle function, instead of the setMasterText function directly. This means I can guarantee saving to local storage whenever the state needs to be updated.
Please see below:
const handleMasterTextUpdate = (newMasterText) => {
localStorage.setItem("mastertext", JSON.stringify(newMasterText));
setMasterText(newMasterText);
};

Reactjs closure when passing state to component

I got a react functional component:
const DataGrid = (props) =>
{
const [containerName, setContainerName] = useState("");
const [frameworkComponents, setFrameworkComponents] = useState(
{customLoadingOverlay: LoadingOverlayTemplate,
customNoRowsOverlay: UxDataGridCustomNoRows,
editButton: params => <ViewAndDeleteSetting {...params}
openAddConfigurationsWindow={openAddConfigurationsWindow}
onDeleteSetting={onDeleteSetting}/>,
});
useEffect(async () =>
{
if(props.containerName && props.containerName !== "")
{
setContainerName(props.containerName);
}
},[props.containerName]);
.
.
.
const onDeleteSetting = async (settingKey) =>
{
console.log("ON DELETE AND CONTAINER NAME:");
console.log(containerName); //HERE THE CONTAINER NAME IS EMPTY
...
}
return (
<UxDataGrid
frameworkComponents={frameworkComponents}/>
);
The container name inside useEffect exists and is not empty. As you can see in the comment in onDeleteSetting, the containerName is empty when this callback is invoked. I tried adding this to the useEffect after setContainerName:
setFrameworkComponents({customLoadingOverlay: LoadingOverlayTemplate,
customNoRowsOverlay: UxDataGridCustomNoRows,
editButton: params => <ViewAndDeleteSetting {...params}
openAddConfigurationsWindow={openAddConfigurationsWindow}
onDeleteSetting={onDeleteSetting}/>,
});
That didn't work.
How can I get the name inside the callback? There is no special need to leave that frameworkComponents struct in the state.. it can also be moved to somewhere else if you think its better
Try this in your useEffect, update the onDeleteSetting function with the new containerName when it's updated
.....
useEffect(async() => {
if (props.containerName && props.containerName !== "") {
setContainerName(props.containerName);
// move this function here
const onDeleteSetting = async(settingKey) => {
console.log("ON DELETE AND CONTAINER NAME:");
// use props.containerName since the state update is async
console.log(props.containerName);
...
}
// update your components with the updated functions
setFrameworkComponents(prevComponents => ({
...prevComponents,
editButton: params =>
<ViewAndDeleteSetting
{...params}
openAddConfigurationsWindow={openAddConfigurationsWindow}
onDeleteSetting={onDeleteSetting}
/>,
}));
}
}, [props.containerName]);
.....
This should provide the updated state with the updated function, if it works, I can add more details.
You almost certainly shouldn't be storing it in state. Props are essentially state controlled by the parent. Just use it from props. Copying props to state is usually not best practice.
If you're looking at one of the very rare situations where it makes sense to set derived state based on props, this page in the documentation tells you how to do that with hooks. Basically, you don't use useEffect, you do your state update right away.
Here's a full quote from the linked documentation:
How do I implement getDerivedStateFromProps?
While you probably don’t need it, in rare cases that you do (such as implementing a <Transition> component), you can update the state right during rendering. React will re-run the component with updated state immediately after exiting the first render so it wouldn’t be expensive.
Here, we store the previous value of the row prop in a state variable so that we can compare:
function ScrollView({row}) {
const [isScrollingDown, setIsScrollingDown] = useState(false);
const [prevRow, setPrevRow] = useState(null);
if (row !== prevRow) {
// Row changed since last render. Update isScrollingDown.
setIsScrollingDown(prevRow !== null && row > prevRow);
setPrevRow(row);
}
return `Scrolling down: ${isScrollingDown}`;
}
This might look strange at first, but an update during rendering is exactly what getDerivedStateFromProps has always been like conceptually.
If you did it the same way they did in that example, your component would still render with containerName set to the default state (""), it's just that it will then almost immediately re-render with the updated containerName. That makes sense for their example of a transition, but you could avoid that by making the prop's initial value the state's initial value, like this:
const DataGrid = (props) => {
const [containerName, setContainerName] = useState(props.containerName); // *** ONLY USES THE INITIAL PROP VALUE
const [frameworkComponents, setFrameworkComponents] = useState(
// ...
});
// *** Updates the state value (on the next render) if the prop changes
if (containerName !== props.containerName) {
setContainerName(props.containerName);
}
// ...
};
Every time the containerName prop changes, though, your component will render twice, which brings us back full circle to: Don't store it in state, just use it from props. :-)
Stepping back and looking at the component as a whole, I don't think you need any state information at all, but if your goal is to avoid having the frameworkComponents you pass UxDataGrid change unnecessarily, you probably want useMemo or React.memo rather than state.
For instance, with useMemo (but keep reading):
const DataGrid = ({containerName}) => {
const frameworkComponents = useMemo(() => {
const onDeleteSetting = async (settingKey) => {
console.log("ON DELETE AND CONTAINER NAME:");
console.log(containerName);
// ...
};
return {
customLoadingOverlay: LoadingOverlayTemplate,
editButton: params => <ViewAndDeleteSetting {...params}
openAddConfigurationsWindow={openAddConfigurationsWindow}
onDeleteSetting={onDeleteSetting} />,
};
}, [containerName]);
return (
<UxDataGrid frameworkComponents={frameworkComponents} />
);
};
But if componentName is your only prop, it may well be even simpler with React.memo:
const DataGrid = React.memo(({containerName}) => {
const onDeleteSetting = async (settingKey) => {
console.log("ON DELETE AND CONTAINER NAME:");
console.log(containerName);
// ...
};
return (
<UxDataGrid frameworkComponents={{
customLoadingOverlay: LoadingOverlayTemplate,
editButton: params => <ViewAndDeleteSetting {...params}
openAddConfigurationsWindow={openAddConfigurationsWindow}
onDeleteSetting={onDeleteSetting} />,
}} />
);
});
React.memo memoizes your component, so that your component function is only ever called again when the props change. Since everything in the component needs to update based on the componentName prop changing, that looks like a good match (but I don't know what UxDataGrid is).
The problem was with how I tried passing props to ViewAndDeleteSetting. If you want to pass prop to a cell rendered component, you shouldn't be doing it in frameworkComponents, but rather you need to do it in the column definition like this:
useEffect(() =>
{
let columns = [{headerName: '', cellRenderer: 'editButton', width: 90, editable: false,
cellRendererParams: {
openAddConfigurationsWindow: openAddConfigurationsWindow,
onDeleteSetting: onDeleteSetting
}},
.. other columns
]
setColumnDefinition(columns);
},[props.containerName]);
The columns with the cellRendererParams do gets recreated in the useEffect when the name changes, and then the component can access this params regularly via its props

My Component not re-render when the store is updated

I've googled but nothing has helped.
I know when in the redux I return the same object, the react Component not re-render. whatever I'm not mutating the store, I'm using connect from 'react-redux' and over other components works right
Thanks :D
//I'm updating a attribute value from this object:
{name: 'some name', score: 0,}
//to
{name: 'some name', score: 1,}
//just change the score
//Component:
class ListTeam extends Component {
render() {
return (
<ListContainer onNewTeam={this.props.onNewTeam}>
{this.props.listTeam.map((item) =>
<Item
name={item.name}
score={item.score}
key={item.toString()}
/>
)}
</ListContainer>
)
}
}
const mapToProps = (store, props) => {
return {
listTeam: store.listTeam
}
}
export default connect(mapToProps)(ListTeam)
REDUCER:
case 'ADD_POINT':{
let newstate = Object.assign({},state); //new state from current state
let current = newstate.currentPlayer //get current player
let listTeam = newstate.listTeam; //getListTeam
listTeam[current].score++; //+1 to score
return {
...state,
listTeam
}
}
What you miss is that in connect provided by react-redux, it only does shallow comparing the current state with previous state. Since you only pass currentPlayer and listTeam from store to your component, and after the action ADD_POINT, the object listTeam is retained with all its keys unchanged, connect logic determined that no update should be made. Here is a small example in redux's github issue thread that's closed to your implementation.
One simple solution is to clone your listTeam in store everytime you update, so that it will be recognized as new object:
case 'ADD_POINT':{
let newstate = Object.assign({},state); //new state from current state
let current = newstate.currentPlayer //get current player
let listTeam = newstate.listTeam; //getListTeam
listTeam[current].score++; //+1 to score
return {
...state,
listTeam: JSON.parse(JSON.stringify(listTeam)
}
}
My simple Codesandbox for above solution: https://codesandbox.io/s/xjp77jpyro
You can also look into Immutable as it's recommended by Redux to handle immutable state.
Another solution is to customize the connect function to force it to recognize change in list: https://react-redux.js.org/api/connect#options-object. But I don't think you need to go that far.

Is it possible to share states between components using the useState() hook in React?

I was experimenting with the new Hook feature in React. Considering I have the following two components (using React Hooks) -
const HookComponent = () => {
const [username, setUsername] = useState('Abrar');
const [count, setState] = useState();
const handleChange = (e) => {
setUsername(e.target.value);
}
return (
<div>
<input name="userName" value={username} onChange={handleChange}/>
<p>{username}</p>
<p>From HookComponent: {count}</p>
</div>
)
}
const HookComponent2 = () => {
const [count, setCount] = useState(999);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
Hooks claim to solve the problem of sharing stateful logic between components but I found that the states between HookComponent and HookComponent2 are not sharable. For example the change of count in HookComponent2 does not render a change in the HookComponent.
Is it possible to share states between components using the useState() hook?
If you are referring to component state, then hooks will not help you share it between components. Component state is local to the component. If your state lives in context, then useContext hook would be helpful.
Fundamentally, I think you misunderstood the line "sharing stateful logic between components". Stateful logic is different from state. Stateful logic is stuff that you do that modifies state. For e.g., a component subscribing to a store in componentDidMount() and unsubscribing in componentWillUnmount(). This subscribing/unsubscribing behavior can be implemented in a hook and components which need this behavior can just use the hook.
If you want to share state between components, there are various ways to do so, each with its own merits:
1. Lift State Up
Lift state up to a common ancestor component of the two components.
function Ancestor() {
const [count, setCount] = useState(999);
return <>
<DescendantA count={count} onCountChange={setCount} />
<DescendantB count={count} onCountChange={setCount} />
</>;
}
This state sharing approach is not fundamentally different from the traditional way of using state, hooks just give us a different way to declare component state.
2. Context
If the descendants are too deep down in the component hierarchy and you don't want to pass the state down too many layers, you could use the Context API.
There's a useContext hook which you can leverage on within the child components.
3. External State Management Solution
State management libraries like Redux or Mobx. Your state will then live in a store outside of React and components can connect/subscribe to the store to receive updates.
It is possible without any external state management library. Just use a simple observable implementation:
function makeObservable(target) {
let listeners = []; // initial listeners can be passed an an argument aswell
let value = target;
function get() {
return value;
}
function set(newValue) {
if (value === newValue) return;
value = newValue;
listeners.forEach((l) => l(value));
}
function subscribe(listenerFunc) {
listeners.push(listenerFunc);
return () => unsubscribe(listenerFunc); // will be used inside React.useEffect
}
function unsubscribe(listenerFunc) {
listeners = listeners.filter((l) => l !== listenerFunc);
}
return {
get,
set,
subscribe,
};
}
And then create a store and hook it to react by using subscribe in useEffect:
const userStore = makeObservable({ name: "user", count: 0 });
const useUser = () => {
const [user, setUser] = React.useState(userStore.get());
React.useEffect(() => {
return userStore.subscribe(setUser);
}, []);
const actions = React.useMemo(() => {
return {
setName: (name) => userStore.set({ ...user, name }),
incrementCount: () => userStore.set({ ...user, count: user.count + 1 }),
decrementCount: () => userStore.set({ ...user, count: user.count - 1 }),
}
}, [user])
return {
state: user,
actions
}
}
And that should work. No need for React.Context or lifting state up
This is possible using the useBetween hook.
See in codesandbox
import React, { useState } from 'react';
import { useBetween } from 'use-between';
const useShareableState = () => {
const [username, setUsername] = useState('Abrar');
const [count, setCount] = useState(0);
return {
username,
setUsername,
count,
setCount
}
}
const HookComponent = () => {
const { username, setUsername, count } = useBetween(useShareableState);
const handleChange = (e) => {
setUsername(e.target.value);
}
return (
<div>
<input name="userName" value={username} onChange={handleChange}/>
<p>{username}</p>
<p>From HookComponent: {count}</p>
</div>
)
}
const HookComponent2 = () => {
const { count, setCount } = useBetween(useShareableState);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
We move React hooks stateful logic from HookComponent to useShareableState.
We call useShareableState using useBetween in each component.
useBetween is a way to call any hook. But so that the state will not be stored in the React component.
For the same hook, the result of the call will be the same. So we can call one hook in different components and work together on one state. When updating the shared state, each component using it will be updated too.
Disclaimer: I'm the author of the use-between package.
the doc states:
We import the useState Hook from React. It lets us keep local state in a function component.
it is not mentioned that the state could be shared across components, useState hook just give you a quicker way to declare a state field and its correspondent setter in one single instruction.
I've created hooksy that allows you to do exactly this - https://github.com/pie6k/hooksy
import { createStore } from 'hooksy';
interface UserData {
username: string;
}
const defaultUser: UserData = { username: 'Foo' };
export const [useUserStore] = createStore(defaultUser); // we've created store with initial value.
// useUserStore has the same signature like react useState hook, but the state will be shared across all components using it
And later in any component
import React from 'react';
import { useUserStore } from './userStore';
export function UserInfo() {
const [user, setUser] = useUserStore(); // use it the same way like useState, but have state shared across any component using it (eg. if any of them will call setUser - all other components using it will get re-rendered with new state)
function login() {
setUser({ username: 'Foo' })
}
return (
<div>
{!user && <strong>You're logged out<button onPress={login}>Login</button></strong>}
{user && <strong>Logged as <strong>{user.username}</strong></strong>}
</div>
);
}
With hooks its not directly possible.
I recommend you to take a look at react-easy-state.
https://github.com/solkimicreb/react-easy-state
I use it in big Apps and it works like a charm.
I'm going to hell for this:
// src/hooks/useMessagePipe.ts
import { useReducer } from 'react'
let message = undefined
export default function useMessagePipe() {
const triggerRender = useReducer((bool) => !bool, true)[1]
function update(term: string) {
message = term.length > 0 ? term : undefined
triggerRender()
}
return {message: message, sendMessage: update}
}
Full explanation over at: https://stackoverflow.com/a/72917627/1246547
Yes, this is the dirtiest and most concise way i could come up with for solving that specific use case. And yes, for a clean way, you probably want to learn how to useContext, or alternatively take a look at react-easy-state or useBetween for low-footprint solutions, and flux or redux for the real thing.
You will still need to lift your state up to an ancestor component of HookComponent1 and HookComponent2. That's how you share state before and the latest hook api doesnt change anything about it.

Categories