How to create a debounce-like function using variables in React? - reactjs

I have a component in React that essentially autosaves form input 3 seconds after the user's last keystroke. There are possibly dozens of these components rendered on my webpage at a time.
I have tried using debounce from Lodash, but that did not seem to work (or I implemented it poorly). Instead, I am using a function that compares a local variable against a global variable to determine the most recent function call.
Curiously, this code seems to work in JSFiddle. However, it does not work on my Desktop.
Specifically, globalEditIndex seems to retain its older values even after the delay. As a result, if a user makes 5 keystrokes, the console.log statement runs 5 times instead of 1.
Could someone please help me figure out why?
import React, {useRef, useState} from "react";
import {connect} from "react-redux";
import {func1, func2} from "../actions";
// A component with some JSX form elements. This component shows up dozens of times on a single page
const MyComponent = (props) => {
// Used to store the form's state for Redux
const [formState, setFormState] = useState({});
// Global variable that keeps track of number of keystrokes
let globalEditIndex = 0;
// This function is called whenever a form input is changed (onchange)
const editHandler = (e) => {
setFormState({
...formState,
e.target.name: e.target.value,
});
autosaveHandler();
}
const autosaveHandler = () => {
globalEditIndex++;
let localEditIndex = globalEditIndex;
setTimeout(() => {
// Ideally, subsequent function calls increment globalEditIndex,
// causing earlier function calls to evaluate this expression as false.
if (localEditIndex === globalEditIndex) {
console.log("After save: " +globalEditIndex);
globalEditIndex = 0;
}
}, 3000);
}
return(
// JSX code here
)
}
const mapStateToProps = (state) => ({
prop1: state.prop1,
prop2: state.prop2
});
export default connect(
mapStateToProps, { func1, func2 }
)(MyComponent);

Note: I was typing up answer on how I solved this previously in my own projects before I read #DrewReese's comment - that seems like a way better implementation than what I did, and I will be using that myself going forward. Check out his answer here: https://stackoverflow.com/a/70270521/8690857
I think you hit it on the head in your question - you are probably trying to implement debounce wrong. You should debounce your formState value to whatever delay you want to put on autosaving (if I'm assuming the code correctly).
An example custom hook I've used in the past looks like this:
export const useDebounce = <T>(value: T, delay: number) => {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value]);
return debouncedValue;
};
// Without typings
const useDebounce = (value, delay) => {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
};
Which you can then use like so:
const [myValue, setMyValue] = useState<number>(0);
const debouncedValue = useDebounce<number>(myValue, 3000);
useEffect(() => {
console.log("Debounced value: ", debouncedValue);
}, [debouncedFormState]);
// without typings
const [myValue, setMyValue] = useState(0);
const debouncedValue = useDebounce(myValue, 3000);
useEffect(() => {
console.log("Debounced value: ", debouncedValue);
}, [debouncedFormState]);
For demonstration purposes I've made a CodeSandbox demonstrating this useDebounce function with your forms example. You can view it here:
https://codesandbox.io/s/brave-wilson-cjl85v?file=/src/App.js

Related

Unsubscribe from listener in hook or in screen component?

I created a hook to manipulate users data and one function is listener for users collection.
In hook I created subscriber function and inside that hook I unsubscribed from it using useEffect.
My question is is this good thing or maybe unsubscriber should be inside screen component?
Does my approach has cons?
export function useUser {
let subscriber = () => {};
React.useEffect(() => {
return () => {
subscriber();
};
}, []);
const listenUsersCollection = () => {
subscriber = firestore().collection('users').onSnapshot(res => {...})
}
}
In screen component I have:
...
const {listenUsersCollection} = useUser();
React.useEffect(() => {
listenUsersCollection();
}, []);
What if I, by mistake, call the listenUsersCollection twice or more? Rare scenario, but your subscriber will be lost and not unsubscribed.
But generally speaking - there is no need to run this useEffect with listenUsersCollection outside of the hook. You should move it away from the screen component. Component will be cleaner and less chances to get an error. Also, easier to reuse the hook.
I prefer exporting the actual loaded user data from hooks like that, without anything else.
Example, using firebase 9 modular SDK:
import { useEffect, useMemo, useState } from "react";
import { onSnapshot, collection, query } from "firebase/firestore";
import { db } from "../firebase";
const col = collection(db, "users");
export function useUsersData(queryParams) {
const [usersData, setUsersData] = useState(undefined);
const _q = useMemo(() => {
return query(col, ...(queryParams || []));
}, [queryParams])
useEffect(() => {
const unsubscribe = onSnapshot(_q, (snapshot) => {
// Or use another mapping function, classes, anything.
const users = snapshot.docs.map(x => ({
id: x.id,
...x.data()
}))
setUsersData(users);
});
return () => unsubscribe();
}, [_q]);
return usersData;
}
Usage:
// No params passed, load all the collection
const allUsers = useUsersData();
// If you want to pass a parameter that is not
// a primitive or a string
// memoize it!!!
const usersFilter = useMemo(() => {
return [
where("firstName", "==", "test"),
limit(3)
];
}, []);
const usersFiltered = useUsersData(usersFilter);
As you can see, all the loading and cleaning-up logic is inside the hook, and the component that uses this hook is as clear as possible.

How to fetch request in regular intervals using the useEffect hook?

What I'm trying to do is fetch a single random quote from a random quote API every 5 seconds, and set it's contents to a React component.
I was able to fetch the request successfully and display it's contents, however after running setInterval method with the fetching method fetchQuote, and a 5 seconds interval, the contents are updated multiple times in that interval.
import { Badge, Box, Text, VStack, Container} from '#chakra-ui/react';
import React, { useState, useEffect } from 'react';
import axios from 'axios';
const RandomQuotes = () => {
const [quote, setQuote] = useState<Quote>(quoteObject);
const [error, setError]: [string, (error: string) => void] = React.useState("");
const [loading, setLoading] = useState(true);
const fetchQuote = () => {
axios.get<Quote>(randomQuoteURL)
.then(response => {
setLoading(false);
setQuote(response.data);
})
.catch(ex => {
setError(ex);
console.log(ex)
});
}
setInterval(() => setLoading(true), 5000);
useEffect(fetchQuote, [loading, error]);
const { id, content, author } = quote;
return (
<>
<RandomQuote
quoteID={id}
quoteContent={content}
quoteAuthor={author}
/>
</>
);
}
When any state or prop value gets updated, your function body will re-run, which is called a re-render.
And you've put setInterval call in the main function(!!!), so each time the component re-renders, it will create another interval again and again. Your browser will get stuck after a few minutes.
You need this interval definition once, which is what useEffect with an empty second parameter is for.
Also, using loading flag as a trigger for an API call works, but semantically makes no sense, plus the watcher is expensive and not needed.
Here's a rough correct example:
useEffect(() => {
const myInterval = setInterval(fetchQuote, 5000);
return () => {
// should clear the interval when the component unmounts
clearInterval(myInterval);
};
}, []);
const fetchQuote = () => {
setLoading(true);
// your current code
};

useEffect in a custom hook freezes my react-native app when the hook is used in more than one place

I have created a custom hook that fetches setting from an api that uses Async-Storage.
// Takes the key/name of the setting to retrieve
export const useSettings = (key) => {
// stores the json string result in the setting variable
const [setting, setSetting] = useState("");
const deviceStorage = useContext(DeviceStorageContext);
useEffect(() => {
getValue()
.then(value => setSetting(value));
}, []);
// gets value from my custom api that uses Async-Storage to handle r/w of the data.
const getValue = async () => await deviceStorage.getValueStored(key);
const setValue = async (value) => {
await deviceStorage.setValueStored(key, value);
getValue().then(value => setSetting(value));
};
const removeValue = async () => { }
return [setting, { setValue,removeValue }];
};
This works as expected in Main.jsx without any problem.
const Main = () => {
const [units, operations] = useSettings('units');
useEffect(() => {
const initSettings = async () => {
if (units) {
console.log(units)
return;
}
await operations.setValue({ pcs: 1, box: 20 });
};
initSettings();
}, []);
However, when I even just call the useSetting hook in Form.jsx and visit the page, it freezes my entire app to just that page.
const FormView = ({ handleReset, handleSubmit }) => {
const [setting,] = useSettings('units');
Removing the useState and useEffect fixes it and calling these methods directly works but I really don't want to call getValue() throughout my project and use async/await code to handle it.
Stuck on this for hours now. Any help will be appreciated.
Thank you.
It was a dropdown component library inside FormView that was messing it up. Removing that library fixed it.

React Redux - useState Hook not working as expected

I have 2 actions in redux (both async) and I'm calling them both within my functional component via dispatch; the first using useEffect and the second via a button click. What I want to do is dispatch the actions to retrieve them from an async function, then use them within my component via useState. But using the useState is not rendering.
Here is my component:
export default function Hello()
{
const { first, second } = useSelector(state => state.myReducer);
const dispatch = useDispatch();
const fetchFirst = async () => dispatch(getFirst());
const fetchSecond = async () => dispatch(getSecond());
const fetchFixturesForDate = (date: Date) => dispatch(getFixturesForDate(date));
const [superValue, setSuperValue] = useState('value not set');
useEffect(() => {
const fetch = async () => {
fetchFirst();
setSuperValue(first);
};
fetch();
}, []);
const getSecondOnClickHandler = async () =>
{
console.log('a')
await fetchSecond();
setSuperValue(second);
}
return (
<div>
<p>The super value should first display the value "first item" once retrieved, then display "second value" once you click the button and the value is retrieved</p>
<p>Super Value: {superValue}</p>
<p>First Value: {first}</p>
<p>Second Value: {second}</p>
<button onClick={async () => await getSecondOnClickHandler()}>Get Second</button>
</div>
)
}
The superValue never renders even though I am setting it, although the value from first and second is retrieved and displayed.
StackBlitz.
Any help?
The value of first and second inside your two useEffects is set when the component mounts (I guess at that point they are undefined). So in both cases you will be setting superValue to that initial value.
You have two options:
Return the first/second values back from fetchFirst and fetchSecond, so that you can retrieve them directly from the executed function, and then set superValue:
useEffect(() => {
const fetch = async () => {
const newFirst = await fetchFirst();
setSuperValue(newFirst);
};
fetch();
}, []);
Add separate useEffects that listen for changes to first and second
useEffect(() => {
setSuperValue(first)
},[first])
useEffect(() => {
setSuperValue(second)
},[second])
The value in the reducer is not necessarily set when the action is dispatched, e.g. after fetchFirst() is called. Also the await that you do in await fetchSecond();
doesn't help since the reducer function is not executed.
You could add useEffect hooks and remove the setSuperValue from the other methods, but I think the code gets quite complicated.
What problem are you trying to solve in the first place?
useEffect(() => setSuperValue(first), [first]);
useEffect(() => setSuperValue(second), [second]);
useEffect(() => {
const fetch = async () => {
fetchFirst();
};
fetch();
}, []);
const getSecondOnClickHandler = async () => {
console.log('a');
await fetchSecond();
};
https://stackblitz.com/edit/react-ts-hsqd3x?file=Hello.tsx

Stream of values with react hooks

I have value X coming from the server. I would like to expose an interface similar to
interface Xclient {
getX(): Promise<X>
}
that I will later use from my react function component.
I say similar because behind the scenes I want it to:
first return value from the storage (its react-native)
simultaneously dispatch network call for newer version of X and re-render the component once I have the response
so instead of Promise I probably need Observable. But then how to use it with react in general and react hooks in particular?
I'm coming from the backend background so there may be some more canonical approach that I dont know about. I really dont want to use redux if possible!
If i understand correctly you have two data source (local storage and your api) and you want to get value from local storage and then get actual value from api. So, you should make next:
import { useState } from "react";
const localProvider: Xclient = new Something();
const apiProvider: Xclient = new SomethingElse();
export function SimpleView() {
const [state, setState] = useState("default value");
localProvider()
.then((response) => {
setState(response);
apiProvider()
.then((actualResponse) => {
setState(actualResponse);
})
.catch(/* */);
})
.catch(/* */);
}
But I see no reason for call it synchronously and you can want to run it parallel:
import { useState } from "react";
const localProvider: Xclient = new Something();
const apiProvider: Xclient = new SomethingElse();
export function SimpleView() {
const [state, setState] = useState("default value");
localProvider()
.then((response) => {
setState(response);
})
.catch(/* */);
apiProvider()
.then((actualResponse) => {
setState(actualResponse);
})
.catch(/* */);
}
If you want to encapsulate this logic you can make a function like this:
import { useState } from "react";
const localProvider: Xclient = new Something();
const apiProvider: Xclient = new SomethingElse();
function getValue(localProvider, apiProvider, consumer) {
localProvider()
.then((response) => {
consumer(response);
})
.catch(/* */);
apiProvider()
.then((actualResponse) => {
consumer(actualResponse);
})
.catch(/* */);
}
export function SimpleView() {
const [state, setState] = useState("default value");
getValue(localProvider, apiProvider, setState);
}
UPD:
As #PatrickRoberts correctly noticed my examples contain the race condition, this code solves it:
import { useState } from "react";
const localProvider: Xclient = new Something();
const apiProvider: Xclient = new SomethingElse();
function getValue(localProvider, apiProvider, consumer) {
let wasApiResolved = false;
localProvider()
.then((response) => {
if (!wasApiResolved) {
consumer(response);
}
})
.catch(/* */);
apiProvider()
.then((actualResponse) => {
wasApiResolved = true;
consumer(actualResponse);
})
.catch(/* */);
}
export function SimpleView() {
const [state, setState] = useState("default value");
getValue(localProvider, apiProvider, setState);
}
I would personally write a custom hook for this to encapsulate React's useReducer().
This approach is inspired by the signature of the function Object.assign() because the sources rest parameter prioritizes later sources over earlier sources as each of the promises resolve:
import { useEffect, useReducer, useState } from 'react';
function useSources<T> (initialValue: T, ...sources: (() => Promise<T>)[]) {
const [fetches] = useState(sources);
const [state, dispatch] = useReducer(
(oldState: [T, number], newState: [T, number]) =>
oldState[1] < newState[1] ? newState : oldState,
[initialValue, -1]
);
useEffect(() => {
let mounted = true;
fetches.forEach(
(fetch, index) => fetch().then(value => {
if (mounted) dispatch([value, index]);
});
);
return () => { mounted = false; };
}, [fetches, dispatch]);
return state;
}
This automatically updates state to reference the result of the latest available source, as well as the index of the source which provided the value, each time a promise resolves.
The reason we include const [fetches] = useState(sources); is so the reference to the array fetches remains constant across re-renders of the component that makes the call to the useSources() hook.
That line could be changed to const fetches = useMemo(() => sources); if you don't mind the component potentially making more than one request to each source during its lifetime, because useMemo() doesn't semantically guarantee memoization. This is explained in the documentation:
You may rely on useMemo as a performance optimization, not as a semantic guarantee. In the future, React may choose to "forget" some previously memoized values and recalculate them on next render, e.g. to free memory for offscreen components.
Here's an example usage of the useSources() hook:
const storageClient: Xclient = new StorageXclient();
const networkClient: Xclient = new NetworkXclient();
export default () => {
const [x, xIndex] = useSources(
'default value',
() => storageClient.getX(),
() => networkClient.getX()
);
// xIndex indicates the index of the source from which x is currently loaded
// or -1 if x references the default value
};

Resources