Preventing of incorrect state with React hooks - reactjs

I have component
import React, { useState, useEffect } from "react";
import { useParams } from "react-router-dom";
const data = {
"1": ["1a", "1b"],
"2": ["2a"]
};
const slugs = {
"1a": { text: "Lorem" },
"1b": { text: "ipsum" },
"2a": { text: "..." }
};
const ExamplePage: React.FC = () => {
const { id } = useParams<{ id: string }>();
const [index, setIndex] = useState(0);
useEffect(() => {
setIndex(0);
}, [id]);
console.log("state", {
id,
index
});
const slug = data[id][index];
const text = slugs[slug].text;
function onPrev(): void {
if (index <= 0) {
return;
}
setIndex((index) => index - 1);
}
function onNext(): void {
if (index >= data[id].length - 1) {
return;
}
setIndex((index) => index + 1);
}
return (
<div>
<button onClick={onPrev}>Prev</button>
<span>{text}</span>
<button onClick={onNext}>Next</button>
</div>
);
};
export default ExamplePage;
And Route for this:
<Route path="/:id" component={ExamplePage} />
Live version: https://codesandbox.io/s/react-hooks-ewl4d
There is a bug with this code when:
User is on /1 url
User clicks button "Next"
User clicks link to /2 url
In this case id will be "2", index will be 1, but there isn't data["2"][1].
As you can see useEffect don't help in this case because useEffect don't stop current function call.
My question is: How I can ensure that this state will be always correct?
I know that I can write const text = slugs[slug]?.text; and this solve my bug, but still, in one moment, component have incorrect state. I wondering if is a way to prevent this incorrect state.
In React class component this problem can be solved by getDerivedStateFromProps - You can see this live on https://codesandbox.io/s/react-hooks-solve-in-react-component-xo43g

The useEffect will run async so you are trying to set the slug and text before the index has updated.
You can put the slug and text into state and then use another useEffect to update them when the index or id changes:
const { id } = useParams();
const [index, setIndex] = useState(0);
const [slug, setSlug] = useState();
const [text, setText] = useState();
useEffect(() => {
setIndex(0);
}, [id]);
useEffect(() => {
const newSlug = data[id][index];
if (!newSlug) return; // If id changes but index has not updated yet
setSlug(newSlug);
setText(slugs[newSlug].text);
}, [id, index]);

Related

React Typewriter effect doesn't reset

I have created a typewriting effect with React and it works perfectly fine. However, when I change the language with i18n both texts don't have the same length and it keeps writing until both texts have the same length and then it changes the language and starts the effect again.
How can I reset the input when the language has changed? How can I reset the input when the component has been destroyed?
I have recorded a video
I have the same issue when I change from one page to another, as both pages have different texts and they don't have the same length.
Here code of my component
export const ConsoleText = ({text, complete = false}) => {
const [currentText, setCurrentText] = useState("");
const translatedText = i18n.t(text);
const index = useRef(0);
useEffect(() => {
if (!complete && currentText.length !== translatedText.length) {
const timeOut = setTimeout(() => {
setCurrentText((value) => value + translatedText.charAt(index.current));
index.current++;
}, 20);
return () => {
clearTimeout(timeOut);
}
} else {
setCurrentText(translatedText);
}
}, [translatedText, currentText, complete]);
return (
<p className="console-text">
{currentText}
</p>
);
};
You are telling react to do setCurrentText(translatedText) only when it is complete or when the compared text lengths are equal, so yes it continues to write until this moment.
To reset your text when text changes, try creating another useEffect that will reset your states :
useEffect(() => {
index.current = 0;
setCurrentText('');
}, [text]);
Now, I actually did this exact same feature few days ago, here is my component if it can help you :
import React from 'react';
import DOMPurify from 'dompurify';
import './text-writer.scss';
interface ITextWriterState {
writtenText: string,
index: number;
}
const TextWriter = ({ text, speed }: { text: string, speed: number }) => {
const initialState = { writtenText: '', index: 0 };
const sanitizer = DOMPurify.sanitize;
const [state, setState] = React.useState<ITextWriterState>(initialState);
React.useEffect(() => {
if (state.index < text.length - 1) {
const animKey = setInterval(() => {
setState(state => {
if (state.index > text.length - 1) {
clearInterval(animKey);
return { ...state };
}
return {
writtenText: state.writtenText + text[state.index],
index: state.index + 1
};
});
}, speed);
return () => clearInterval(animKey);
}
}, []);
// Reset the state when the text is changed (Language change)
React.useEffect(() => {
if (text.length > 0) {
setState(initialState);
}
}, [text])
return <div className="text-writer-component"><span className="text" dangerouslySetInnerHTML={{ __html: sanitizer(state.writtenText) }} /></div>
}
export default TextWriter;
The translation is made outside of the component so you can pass any kind of text to the component.

React Hooks reducer causing memory leak

I have a React component that calls a reducer on initialisation to populate an array with as many blank records as a number specified in a firebase db. If the firebase db field myArray length is 5, the component will initialise and call the reducer to create 5 blank records in an array in the context:
import React, { useState, useEffect, useContext } from 'react';
import { useHistory, useLocation } from 'react-router';
import ArrayCard from '../../../containers/ArrayCard';
import firebase from '../../../Utils/firebase';
import MyContext from '../../../Utils/contexts/MyContext ';
import { initialiseMyArray, changeArray} from '../../../Utils/reducers/MyActions';
function MyComponent() {
const { push } = useHistory();
const location = useLocation();
const context = useContext(MyContext);
const dispatch = context.dispatch;
const session = location.state.currentSession;
const sessionRef = firebase.firestore().collection("sessions").doc(session);
const [ loaded, setLoaded ] = useState(false);
const [ myArray, setMyArray] = useState([]);
const [ myArrayCurrentIndex, setMyArrayCurrentIndex ] = useState();
const getSessionData = () => {
sessionRef.get().then(function(doc) {
if (doc.exists) {
const myArrayLength= parseInt(doc.data().myArrayLength);
dispatch({ type: initialisePensionsFromFirebase, myArrayLength: myArrayLength});
setMyArrayCurrentIndex (0);
setLoaded(true);
} else {
// doc.data() will be undefined in this case
console.log("No such document!");
}
}).catch(function(error) {
console.log("Error getting document:", error);
});
}
useEffect(() => {
if (sessionRef && !loaded && myArray.length < 1) {
getSessionData();
}
if (context) {
console.log("CONTEXT ON PAGE: ", context)
}
}, [loaded, currentIndex])
The index currentIndex of the newly-created myArray in the context is used to populate a component inside this component:
return (
<innerComponent
handleAmendSomeproperty={(itemIndex, field, value) => handleAmendSomeproperty(itemIndex, field, value)}
itemIndex={currentIndex}
someproperty={context.state.someitem[currentIndex].someproperty}
></innerComponent>
)
I want to be able to amend the someproperty field in myArray[currentIndex], but my dispatch causes endless rerenders.
const handleAmendSomeproperty= (itemIndex, field, value) => {
dispatch({ type: changeItem, itemIndex: itemIndex});
}
My reducer switch cases are like so:
case initialiseMyArray:
console.log("SRSTATE: ", state);
let _initialArray = []
for (let i=0; i<action.myArrayLength; i++) {
_initialArray .push(
{
itemIndex: i,
someproperty: "" }
)
}
return {
...state,
someproperty: [..._initialArray ]
};
case changeArray:
// I want to leave the state untouched at this stage until I stop infinite rerenders
return {
...state
};
What is happening to cause infinite rerenders? How come I can't amend someproperty, but initialising the new array in the state works fine?
innerComponent has lots of versions of this:
<div>
<label>{label}</label>
<div onClick={() => handleAmendSomeproperty(itemIndex, "fieldName", "fieldValue")}>
{someproperty === "booleanValue" ? filled : empty}
</div>
</div>
and lots like this:
<input
type="text"
placeholder=""
value={somepropertyvalue}
onChange={(event) => handleAmendSomeproperty(itemIndex, "fieldName", event.target.value)}
/>

React Sequential Rendering Hook

I've got some components which need to render sequentially once they've loaded or marked themselves as ready for whatever reason.
In a typical {things.map(thing => <Thing {...thing} />} example, they all render at the same time, but I want to render them one by one I created a hook to to provide a list which only contains the sequentially ready items to render.
The problem I'm having is that the children need a function in order to tell the hook when to add the next one into its ready to render state. This function ends up getting changed each time and as such causes an infinite number of re-renders on the child components.
In the examples below, the child component useEffect must rely on the dependency done to pass the linter rules- if i remove this it works as expected because done isn't a concern whenever it changes but obviously that doesn't solve the issue.
Similarly I could add if (!attachment.__loaded) { into the child component but then the API is poor for the hook if the children need specific implementation such as this.
I think what I need is a way to stop the function being recreated each time but I've not worked out how to do this.
Codesandbox link
useSequentialRenderer.js
import { useReducer, useEffect } from "react";
const loadedProperty = "__loaded";
const reducer = (state, {i, type}) => {
switch (type) {
case "ready":
const copy = [...state];
copy[i][loadedProperty] = true;
return copy;
default:
return state;
}
};
const defaults = {};
export const useSequentialRenderer = (input, options = defaults) => {
const [state, dispatch] = useReducer(options.reducer || reducer, input);
const index = state.findIndex(a => !a[loadedProperty]);
const sliced = index < 0 ? state.slice() : state.slice(0, index + 1);
const items = sliced.map((item, i) => {
function done() {
dispatch({ type: "ready", i });
return i;
}
return { ...item, done };
});
return { items };
};
example.js
import React, { useEffect, useState } from "react";
import ReactDOM from "react-dom";
import { useSequentialRenderer } from "./useSequentialRenderer";
const Attachment = ({ children, done }) => {
const [loaded, setLoaded] = useState(false);
useEffect(() => {
const delay = Math.random() * 3000;
const timer = setTimeout(() => {
setLoaded(true);
const i = done();
console.log("happening multiple times", i, new Date());
}, delay);
return () => clearTimeout(timer);
}, [done]);
return <div>{loaded ? children : "loading"}</div>;
};
const Attachments = props => {
const { items } = useSequentialRenderer(props.children);
return (
<>
{items.map((attachment, i) => {
return (
<Attachment key={attachment.text} done={() => attachment.done()}>
{attachment.text}
</Attachment>
);
})}
</>
);
};
function App() {
const attachments = [1, 2, 3, 4, 5, 6, 7, 8].map(a => ({
loaded: false,
text: a
}));
return (
<div className="App">
<Attachments>{attachments}</Attachments>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Wrap your callback in an aditional layer of dependency check with useCallback. This will ensure a stable identity across renders
const Component = ({ callback }) =>{
const stableCb = useCallback(callback, [])
useEffect(() =>{
stableCb()
},[stableCb])
}
Notice that if the signature needs to change you should declare the dependencies as well
const Component = ({ cb, deps }) =>{
const stableCb = useCallback(cb, [deps])
/*...*/
}
Updated Example:
https://codesandbox.io/s/wizardly-dust-fvxsl
Check if(!loaded){.... setTimeout
or
useEffect with [loaded]);
useEffect(() => {
const delay = Math.random() * 1000;
const timer = setTimeout(() => {
setLoaded(true);
const i = done();
console.log("rendering multiple times", i, new Date());
}, delay);
return () => clearTimeout(timer);
}, [loaded]);
return <div>{loaded ? children : "loading"}</div>;
};

React | useState from another useState, changes are not propagated, how could I refactor it?

Having a state that changes after a while (after fetching content), if I want to construct a variable and build a new state from that, when the first state changes it is not propagated to the second state
How can I solve this? I wouldn't like to merge the 2 states into one since this would mix components that do different things
If you have time you can take a look at the codesandbox below
https://codesandbox.io/s/purple-sun-mx428?fontsize=14&hidenavigation=1&theme=dark
And I paste here the code
import React, { useEffect, useState } from "react";
import "./styles.css";
const wait = ms => new Promise((r, j) => setTimeout(r, ms));
const test2 = () => {
const [test, setTest] = useState("hi");
useEffect(() => {
const testf = async stillMounted => {
await wait(1000);
setTest("bye");
};
const stillMounted = { value: true };
testf(stillMounted);
return () => {
stillMounted.value = false;
};
}, []);
return test;
};
export default function App() {
const here = test2();
const stru = [{ id: "whatever", content: here }];
const [elements, setElements] = useState(stru);
console.log(stru);
console.log(elements);
return (
<div className="App">
<h1>stru: {stru[0].content}</h1>
<h2>elements: {elements[0].content}</h2>
</div>
);
}
You can write an effect that runs when the here variable changes. Add something like this to your App component.
useEffect(() => {
// This runs when 'here' changes and then updates your other state
setElements([{ id: "whatever", content: here }])
}, [here]) // This makes this effect dependent on the 'here' variable

How to get value from useState inside the function

I am trying to build Hanging man game and want to get value from useState inside the checkMatchLetter function, but not sure if that is possible and what I did wrong....
import React, { useState, useEffect } from 'react';
import { fetchButton } from '../actions';
import axios from 'axios';
import 'babel-polyfill';
const App = () => {
const [word, setWord] = useState([]);
const [underscore, setUnderscore] = useState([]);
const [data, setData] = useState([]);
useEffect(() => {
const runEffect = async () => {
const result = await axios('src/api/api.js');
setData(result.data)
}
runEffect();
}, []);
const randomWord = () => {
const chosenWord = data[Math.floor(Math.random() * data.length)];
replaceLetter(chosenWord.word);
}
const replaceLetter = (string) => {
let getString = string; // here it shows a valid string.
setWord(getString);
let stringToUnderScore = getString.replace(/[a-z]/gi, '_');
setUnderscore(stringToUnderScore);
}
useEffect(() => {
const checkLetter = (event) => {
if(event.keyCode >= 65 && event.keyCode <= 90) {
checkMatchLetter(word, String.fromCharCode(event.keyCode).toLowerCase());
}
};
document.addEventListener('keydown', checkLetter);
return () => {
document.removeEventListener('keydown', checkLetter);
}
}, []);
const checkMatchLetter = (keyButton) => {
console.log(keyButton);
let wordLength = word.length;
console.log(wordLength); // here it outputs '0'
/// here I want word of useState here....
}
return (
<div>
<p>{word}</p>
<p>{underscore}</p>
<button onClick={randomWord}></button>
</div>
)
}
export default App;
The reason why I want to obtain that value inside this function is so I can compare the clicked keybutton (a-z) to the current chosenword. And if there is something wrong with other functions, please feel free to share your feedback here below as well.
You're using a variable defined inside the component render function in a useEffect effect and that variable is missing in the hook's deps. Always include the deps you need (I highly recommend the lint rule react-hooks/exhaustive-deps). When you add checkMatchLetter to deps you'll always have the newest instance of the function inside your effect instead of always using the old version from the first render like you do now.
useEffect(() => {
const checkLetter = (event) => {
if(event.keyCode >= 65 && event.keyCode <= 90) {
checkMatchLetter(word, String.fromCharCode(event.keyCode).toLowerCase());
}
};
document.addEventListener('keydown', checkLetter);
return () => {
document.removeEventListener('keydown', checkLetter);
}
}, [checkMatchLetter, word]);
This change will make the effect run on every render. To rectify that, you can memoise your callbacks. However, that's a new can of worms.

Resources