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

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);
};

Related

Invalid hook call in work with local Storage

I have a 3 files:
Main component,
File with states that are stored in local storage
A file with a reset function for resetting these same states to default
values.
I import the file with the states and reset file in the main component and everything is ok. But when I try use reset function for set localState value to default, i got error “Error: Invalid hook call. Interceptors can only be called inside the body of a functional component. "
I read the documentation on react, but I did not understand the error
First file code:
import React from "react";
import { LocalStorage } from "./localState";
import { resetLocalStorage } from "./resetLocalState";
function App() {
const localState = LocalStorage(); // local storage keys
const resetState = () => resetLocalStorage(); // reset local storate states
return (
<div>
<button onClick={() => resetState()}>Refresh State to default</button>
<br />
<button
onClick={() => localState.setLocalStorageState("State was changed")}
>
Change State
</button>
<p>{localState.localStorageState}</p>
</div>
);
}
export default App;
Second file code:
import { useState, useEffect } from "react";
const useLocalStorageList = (key, defaultValue) => {
const stored = localStorage.getItem(key);
const initial = stored ? JSON.parse(stored) : defaultValue;
const [value, setValue] = useState(initial);
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
};
//local storage keys
export const LocalStorage = () => {
const [localStorageState, setLocalStorageState] = useLocalStorageList(
"State",
"Default Value"
);
return { localStorageState, setLocalStorageState };
};
Third file code
import { LocalStorage } from "./localState";
export const resetLocalStorage = () => {
const localState = LocalStorage(); //local storage keys
localState.setLocalStorageState("Default Value");
};
Link to SandBox
I didnt see anything to reset all states in your resetLocalStorage(). I assume you will keep track of all the 'local storage keys' and define reset-functions for each. This example modifies your hook to return a third function to reset the state so another reset-function doesn't have top be defined.
https://codesandbox.io/s/smoosh-sound-0yxvl?file=/src/App.js
I made few changes in your code to achieve the use case you were trying to achieve. (Let me know my implementation is suffices your use case)
The resetLocalStorage is not starting with use That's why you were getting the previous error.
After renaming that function to useResetLocalStorage, still it will not work since -
you were calling the custom hook onClick of button. This breaks one react hook rule which is react hooks must not be called conditionally https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

How to trigger useEffects before render in React?

I have a prop being passed from a parent component to a child component which changes based on the user's input.
I want to trigger a data fetch in the child component when that prop changes before the child component is rendered. How can I do it?
I tried in the following manner by using useEffects(()=>{},[props.a, props.b]) but that is always called after the render. Please help!
import React, { useEffect, useState } from "react";
import "./styles.css";
export default function parentComponent() {
const [inputs, setInputs] = useState({ a: "", b: "" });
return (
<>
<input
value={inputs.a}
onChange={(event) => {
const value = event.target.value;
setInputs((prevState) => {
return { ...prevState, a: value };
});
}}
/>
<input
value={inputs.b}
onChange={(event) => {
const value = event.target.value;
setInputs((prevState) => {
return { ...prevState, b: value };
});
}}
/>
<ChildComponent a={inputs.a} b={inputs.b} />
</>
);
}
function ChildComponent(props) {
const [isLoading, setIsLoading] = useState(true);
const [data, setData] = useState({});
useEffect(() => {
console.log("updating new data based on props.a: " + props.a);
setData({ name: "john " + props.a });
return () => {};
}, [props.a, props.b]);
useEffect(() => {
console.log("data successfully changed");
console.log(data);
if (Object.keys(data).length !== 0) {
setIsLoading(false);
}
return () => {};
}, [data]);
function renderPartOfComponent() {
console.log("rendering POC with props.a: " + props.a);
return <div>data is: {data.name}</div>;
}
return (
<div className="App">{isLoading ? null : renderPartOfComponent()}</div>
);
}
In the console what I get is:
rendering POC with props.a: fe
rendering POC with props.a: fe
updating new data based on props.a: fe
rendering POC with props.a: fe
rendering POC with props.a: fe
data successfully changed
Object {name: "john fe"}
rendering POC with props.a: fe
rendering POC with props.a: fe
If you know how I can make the code more efficient, that would be a great help as well!
Here's the codesandbox link for the code: https://codesandbox.io/s/determined-northcutt-6z9f8?file=/src/App.js:0-1466
Solution
You can use useMemo, which doesn't wait for a re-render. It will execute as long as the dependencies are changed.
useMemo(()=>{
doSomething() //Doesn't want until render is completed
}, [dep1, dep2])
You can use function below:
// utils.js
const useBeforeRender = (callback, deps) => {
const [isRun, setIsRun] = useState(false);
if (!isRun) {
callback();
setIsRun(true);
}
useEffect(() => () => setIsRun(false), deps);
};
// yourComponent.js
useBeforeRender(() => someFunc(), []);
useEffect is always called after the render phase of the component. This is to avoid any side-effects from happening during the render commit phase (as it'd cause the component to become highly inconsistent and keep trying to render itself).
Your ParentComponent consists of Input, Input & ChildComponent.
As you type in textbox, ParentComponent: inputs state is modified.
This state change causes ChildComponent to re-render, hence renderPartOfComponent is called (as isLoading remains false from previous render).
After re-render, useEffect will be invoked (Parent's state propagates to Child).
Since isLoading state is modified from the effects, another rendering happens.
I found the solution by creating and maintaining state within the ChildComponent
So, the order of processes was this:
props modified -> render takes place -> useEffect block is executed.
I found the workaround by simply instantiating a state within the childComponent and making sure that the props state is the same as the one in the child component before rendering, else it would just show loading... This works perfectly.
Nowadays you can use useLayoutEffect which is a version of useEffect that fires before the browser repaints the screen.
Docs: https://beta.reactjs.org/reference/react/useLayoutEffect

local state values cant be equal to redux state values

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.

Can not invoke callback defined in beforeDestory hook

I am trying to write a simple todoList using vue.js and I want to save those todos into cookies before the vue instance is destroyed. But I find it weird that though I wrote callback in beforeDestory hook, the hook is never called.
I checked Vue documents and could not find any hint.
when I tried to
save those todos into cookies by adding callback to window.onbeforeunload and window.onunload, it works.
my code is like
computed: {
todos() {
return this.$store.getters.todos
},
...
},
beforeDestroy() {
myStorage.setTodos(this.todos)
}
todos is a array defined in store.js, which has been imported in main.js, like
import myStorage from '#/utils/storage'
...
const store = new Vuex.Store({
state: {
todos: myStorage.getTodos()
...
},
getters: {
todos: state => state.todos
}
and myStorage is defined as:
import Cookies from 'js-cookie'
const todoKey = 'todo'
const setTodos = (todos) => {
Cookies.set(todoKey, JSON.stringify(todos))
}
const getTodos = () => {
const todoString = Cookies.get(todoKey)
let result = []
if (todoString) {
const todoParsed = JSON.parse(todoString)
if (todoParsed instanceof Array) {
result = todoParsed
}
}
return result
}
export default {
setTodos: setTodos,
getTodos: getTodos
}
I am using vue 2.6.10, and my project is constructed by vue-cli3.
I develop this todolist using Chrome on Window 10.
I expect that after I close the window or after I refresh the window, the todolist can still fetch todo written previously from cookies. But the fact is that the beforeDestory hook is never called.
When you refresh the window, the component's beforeDestroy() is not called, because you are not programmatically destroying the component, but ending the entire browser session.
A better solution would simply to call myStorage.setTodos whenever the todos object in the component is mutated. You can do that by setting up a watcher for the computed property:
computed: {
todos() {
return this.$store.getters.todos
},
},
watch: {
todos() {
myStorage.setTodos(this.todos)
}
}
Altertively, you let the VueX store handle the storage. It is unclear from your question if you are mutating the todos state: if you are mutating it, you can also do myStorage.setTodos in the store. The actual component can be dumb in that sense, so that all it needs to do is to update the store.

Child Component Not Getting New Data When Redux Updated By Parent Component

I have looked at other questions that seemingly had a similar issue, but none of the accepted answers have solved my issue. I am attempting to fetch new names and load them into child component when redux is updated with new IDs.
When I use only redux and no state (as I would prefer), the new IDs do not get passed along to the child component and the names do not load at all
Alternatively, I have tried using state for the names in the child component (as you can see in the commented text below). However ... Oddly enough, every time the IDs are changed, the component loads the names based on the previous IDs rather than the current IDs.
Redux
const initialState = {
objectOfIds: {"someID":"someID", "aDifferentID":"aDifferentID"}, // I know this format may seem redundant and odd, but I have to keep it this way
arrayOfNames: ["John Doe", "Jenny Smith"]
}
Parent Compoenent
// React
import React from 'react';
import firebase from 'firebase';
// Components
import ListOfNames from './ListOfNames';
// Redux
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {set} from './../actions/index.js';
class ParentComponent extends React.Component {
constructor(props) {
super(props);
this.changeIDs = this.changeIDs.bind(this);
}
changeIDs() {
this.props.set("objectOfIds",{"aNewID":"aNewID","someOtherID":"someOtherID","anotherID":"anotherID"});
}
render (
return (
<div>
<h2>Parent Component</h2>
<button onClick={this.changeIDs}>Change Data</button>
<ListOfNames objectOfIds={this.props.reduxData.objectOfIds}/>
</div>
)
)
}
function mapStateToProps(state) {
return {
reduxData: state.reduxData
};
}
function matchDispatchToProps(dispatch) {
return bindActionCreators({
set: set
}, dispatch)
}
export default connect(mapStateToProps, matchDispatchToProps)(ParentComponent);
Child Compoenent
// React
import React from 'react';
import firebase from 'firebase';
// Redux
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {set} from './../actions/index.js';
// Firebase Database
var databaseRef;
class ListOfNames extends React.Component {
constructor(props) {
super(props);
this.state= {
arrayOfNames: []
}
this.fetchNamesForIds = this.fetchNamesForIds.bind(this);
this.add = this.add.bind(this);
}
componentDidMount() {
console.log("componentDidMount triggering...");
firebase.auth().onAuthStateChanged(function (user) {
if (!user) {
console.log("no user authenticated");
}
databaseRef = firebase.database().ref('/people/' + user.uid);
this.fetchNamesForIds(this.props.reduxData.objectOfIds);
})
}
// I have tried making the fetch in componentWillReceiveProps so that the function would run anytime the IDs were updated in redux, but "this.props.objectOfIds" and "this.props.reduxData.objectOfIds"
componentWillReceiveProps() {
console.log("componentWillReceiveProps triggering...");
console.log("this.props.objectOfIds");
console.log(this.props.objectOfIds);
console.log("this.props.reduxData.objectOfIds");
console.log(this.props.reduxData.objectOfIds);
this.fetchNamesForIds(this.props.reduxData.objectOfIds);
// Note: I have also tried: this.fetchNamesForIds(this.props.objectOfIds); so that the data is passed in from the parent
}
// fetched the names for the associated IDs
fetchNamesForIds(personIds) {
if (personIds === [] || personIds === undefined || personIds === null) {
ALTERNATIVE TO LINE ABOVE
I would prefer to store the data in redux so that it is accessible to other components, but doing this did allow the data to load, but it loads with a lag (i.e. when I change the IDs, it loads the names associated to the previous IDs)
// this.setState({
// arrayOfNames: []
// });
this.props.set("arrayOfNames", []);
return
}
var arrayOfNames = [];
// loop through person and set each value into the arrayOfNames array
Object.keys(IDs).map(function(person, index) {
console.log("person = " + person);
console.log("index = " + index);
// get names associated with the ids obtained
var name = ''
databaseRef.child('people').child(person).limitToFirst(1).on("value", function(snapshot) {
var firstName = snapshot.child('firstName').val()
var lastName = snapshot.child('firstName').val()
name = firstName + " " + lastName
console.log("name = " + name);
arrayOfNames.push(name);
console.log("arrayOfNames = " + arrayOfNames);
this.props.set("arrayOfNames", arrayOfNames);
ALTERNATIVE TO LINE ABOVE
I would prefer to store the data in redux so that it is accessible to other components, but doing this did allow the data to load, but it loads with a lag (i.e. when I change the IDs, it loads the names associated to the previous IDs)
// this.setState({
// arrayOfNames: arrayOfNames
// });
}.bind(this));
}.bind(this));
}
render() {
return(
(this.props.user.arrayOfNames === [] || this.props.user.arrayOfNames === undefined || this.props.user.arrayOfNames === null || this.props.user.arrayOfNames.length < 1)
? <span>no people selected</span>
: <div>
<h5>List of People</h5>
{this.props.user.arrayOfNames.map((name, index) => {
return (
<h5>{name}</h5>
)
})}
</div>
)
}
}
ListOfNames.propsTypes = {
objectOfIds: React.PropTypes.Object
};
function mapStateToProps(state) {
return {
reduxData: state.reduxData
};
}
function matchDispatchToProps(dispatch) {
return bindActionCreators({
set: set
}, dispatch)
}
export default connect(mapStateToProps, matchDispatchToProps)(ListOfNames);
Similar Questions:
https://github.com/gaearon/redux-thunk/issues/80
React Native Child Component Not Updated when redux state changes
update child component when parent component changes
Does anyone understand how I can get my component to load the data based on the current IDs in redux?
Probably because the object keys are changed but not the object reference.
A hacky solution would be to call this.forceUpdate() to update the component after the change:)
I had a similar issue where I was loading a child component multiple times on one page and despite passing in what I thought was a unique ID it would only reference the first ID. I know this isn't exactly the situation you have but this will allow you to have a unique object key AND a unique object reference which will hopefully fix your issue.
This is the package I used for this: https://www.npmjs.com/package/react-html-id. When you import the package you need to have curly brackets.
import { enableUniqueIds } from 'react-html-id'
The rest is explained on npm.
Tip: you don't need to bind your functions if you use the new javascript syntax.
this.add = this.add.bind(this); will be solved by writting the add method like:
add = () => {
};
The issue is the Child Component componentWillReceiveProps. You are not using the new props that are propagated to this component. componentWillReceiveProps is called with nextProps, which contains the updated props.
Use this in your child component
componentWillReceiveProps(nextProps) {
console.log("componentWillReceiveProps triggering...");
console.log("nextProps.objectOfIds ", nextProps.objectOfIds);
console.log("nextProps.reduxData.objectOfIds ", nextProps.reduxData.objectOfIds);
this.fetchNamesForIds(nextProps.reduxData.objectOfIds);
}

Categories