I have a function that filters through some state and renders out the result for a search request.
const handleSearch = (value: string) => {
const searchResultData = users.filter((userId) => user.id.startsWith(value));
setSearchResult(searchResultData);
};
I am trying to work with lodash.throttle library to cause a delay before the request is sent. So we don't have a request go out every time a user types.
const handleSearch = useCallback(throttle((value: string) => {
const searchResultData = users.filter((userId) => user.id.startsWith(value));
setSearchResult(searchResultData);
}, 2500), []);
This works in delaying input as expected but for some reason, the user.filter method doesn't run, and so the state isn't updated with the search result. I believe the problem might be from the useCallback hook, but the throttle function is dependent on it to run. Any ideas on how I can work around this problem?
If your throttled/debounced handler uses props or state, like this:
const { fetcherFunctionFromProps } = props;
const eventHandler = async () => {
const resp = await fetcherFunctionFromProps();
};
const debouncedEventHandler = useMemo(
() => throttle(eventHandler, 300)
), [fetcherFunctionFromProps]);
And it doesn't work,
you can refactor it to the following:
const { fetcherFunctionFromProps } = props;
const eventHandler = async (fetcher) => {
const resp = await fetcher();
};
const debouncedEventHandler = useMemo(() => throttle(eventHandler, 300), []);
...
<Component onClick={() => debouncedEventHandler(fetcherFunctionFromProps)}>
Related
I just got to know that setState is a async call.
I want to use the result updated by setState in a very next line but as it is a async call I am getting old result of state. I check lot of solutions but they are mostly class based react and I am using functional based component, class based component setState do have a second args which can be use as a callback but what about functional based?
How & how many approaches do we have to tackle this issue.
here is my code, I want to use filtersAdded state result in a next line to update another state...
const { data, setData } = useContext(defaultData);
const [pestleFilters, setPestleFilters] = useState([]);
const [filtersAdded, setFiltersAdded] = useState([]);
let flag = 0;
let availableFilters = [];
const [toggleFilter, setToggleFilter] = useState(false);
const addFilter = (val) => {
let filters = [...filtersAdded];
filters.push(val);
setFiltersAdded(filters);
let pestleFiltersAux = [...pestleFilters].filter((item) => {
if (!filters.includes(item)) {
return { item };
}
});
setPestleFilters(pestleFiltersAux, () => {
console.log(filtersAdded);
});
let temp = data.filter((d) => filtersAdded.includes(d.pestle));
console.log(temp);
};
Why don't you use filters variable if it is equal to filtersAdded ?
let temp = data.filter((d) => filters.includes(d.pestle));
console.log(temp);
You can't access a state that you just set in the same function, one way you could do this is by creating a useEffect that listen to your state that you just set, like in the example below:
const [state, setState] = useState(0)
const [otherState, setOtherState] = useState(0)
const func = () => {
setState(3)
}
useEffect(() => {
setOtherState(state + 1)
}, [state])
Once you will call the func(), the state will be updated to 3 and useEffect will throw his function because it listen to "state", so otherState will be updated to 4.
Goal is to display a real time log that comes from an async function ( this func will return the last log ) and is updated each second. After that i want to accumulate the results on an array and if it get bigger than 5 i want to remove the first element.
Expected: A list of 5 items displayed on screen that is updated each second.
Results: Array is not accumulating above 2 items and the update is random and fast
code ->
const [logs, setLogs] = useState([])
const getLogs = async () => {
const lastLog = await window.getAppLogs()
if (logs.length > 5) {
// here i tried this methods ->
// reduceLogs.shift()
// reduceLogs.splice(0, 1)
const reduceLogs = [...logs, lastLog ]
delete reduceLogs[0]
return setLogs(reduceLogs)
}
const test = [...logs, lastLog] // this line is not accumulating above 2
setLogs(test)
}
useEffect(() => {
setInterval(getLogs, 1000);
}, [])
Updating state in setinterval needs to use the => syntax. So it should be -
setLogs(prevState => [...prevState.slice(-4), lastLog])
Plus you need to clear the interval as well. I've made a demo to display last 5 users when they are updated every second (from api).
export default function App() {
const [users, setUsers] = useState([
"michael",
"jack",
]);
const getUser = async () => {
const response = await fetch("https://randomuser.me/api/?results=1");
const user = await response.json();
return user;
};
const getUsers = async () => {
const user = await getUser();
setUsers(prevState => [...prevState.slice(-4), user.results[0].name.first]);
};
useEffect(() => {
const handle = setInterval(getUsers, 1000);
return () => {
clearInterval(handle);
};
}, []);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
{users.map((log) => (
<div>{log}</div>
))}
</div>
);
}
Update: This should be your getLogs function, no if check, no return etc.
const getLogs = async () => {
const lastLog = await window.getAppLogs()
setLogs((prevState) => [...prevState.slice(-4), lastLog]);
}
hey AG its almost working perfectly, updates are constant now and it is accumulating. But is still not reducing length of the array. Its like u show me:
const [logs, setLogs] = useState([])
const getLogs = async () => {
const lastLog = await window.getAppLogs()
if (logs.length > 4) {
return setLogs((prevState) => [...prevState.slice(-4), lastLog])
}
setLogs((prevState) => [...prevState, lastLog])
}
useEffect(() => {
const handle = setInterval(getLogs, 2500);
return () => {
clearInterval(handle);
};
}, [])
I am trying to execute a function to update a setState but it as well needs other state to load first.
const [chatsIds, setChatsIds] = useState([]);
const [chats, setChats] = useState([]);
useEffect(() => {
getChatsIds();
}, []);
useEffect(() => {
getChats();
}, [chats]);
the "getChats" needs the value from "chatsIds" but when the screen is loaded the value isn't , only when i reload the app again it gets the value.
Here are the functions :
const getChatsIds = async () => {
const ids = await getDoc(userRef, "chats");
setChatsIds(ids);
}
const getChats = async () => {
const chatsArr = [];
chatsIds.forEach(async (id) => {
const chat = await getDoc(doc(db, "Chats", id));
chatsArr.push(chat);
console.log(chatsArr);
});
setChats(chatsArr);
}
I've tried with the useEffect and useLayoutEffect hooks, with promises and async functions, but i haven't found what i'm doing wrong :(
The problem is in your useEffect hook dependency. It should depends on chatsIds not chats.
useEffect(() => {
getChats();
}, [chatsIds]);
Which mean fetching chatsIds should depend on first mount and fetching chats should depend on if chatsIds is chnaged.
You simply change the useEffect hook to like below.
useEffect(() => {
getChatsIds();
}, [chatsIds]);
I Think getChat() is depend on chatIds...
so you use useEffect with chatIds on dependency
const [chatsIds, setChatsIds] = useState([]);
const [chats, setChats] = useState([]);
useEffect(() => {
getChatsIds();
}, []);
useEffect(() => {
getChats(chatsIds);
}, [chatsIds]);
const getChatsIds = async () => {
const ids = await getDoc(userRef, "chats");
setChatsIds(ids);
}
const getChats = async (chatsIds) => {
const chatsArr = [];
chatsIds.forEach(async (id) => {
const chat = await getDoc(doc(db, "Chats", id));
chatsArr.push(chat);
console.log(chatsArr);
});
setChats(chatsArr);
}
When state is in a hook it can become stale and leak memory:
function App() {
const [greeting, setGreeting] = useState("hello");
const cb = useCallback(() => {
alert("greeting is " + greeting);
}, []);
return (
<div className="App">
<button onClick={() => cb()}>Click me</button>
<p>
Click the button above, and now update the greeting by clicking the one
below:
</p>
<button onClick={() => setGreeting("bye")}>
Update greeting
</button>
<p>Greeting is: {greeting}</p>
<p>
Now click the first button again and see that the callback still has the
old state.
</p>
</div>
);
}
Demo: https://codesandbox.io/s/react-hook-stale-datamem-leak-demo-9pchk
The problem with that is that we will run into infinite loops in a typical scenario to fetch some data if we follow Facebook's advice to list all dependencies always, as well as ensure we don't have stale data or memory leaks (as the example showed above):
const [state, setState] = useState({
number: 0
});
const fetchRandomNumber = useCallback(async () => {
if (state.number !== 5) {
const res = await fetch('randomNumber');
setState(v => ({ ...v, number: res.number }));
}
}, [setState, state.number]);
useEffect(() => {
fetchRandomNumber();
}, [fetchRandomNumber]);
Since Facebook say we should list fetchRandomNumber as a dependency (react-hooks/exhaustive-deps ESLint rule) we have to use useCallback to maintain a reference, but it regenerates on every call since it both depends on state.number and also updates it.
This is a contrived example but I've run into this many times when fetching data. Is there a workaround for this or is Facebook wrong in this situation?
Use the functional form of the state setter:
const fetchData = useCallback(async () => {
const res = await fetch(`url?page=${page}`);
setData((data) => ([...data, ...res.data]));
setPage((page) => page + 1);
}, [setData, setPage]);
Now you don't need data and page as your deps
You can also use a ref to run the effect only on mount :
const mounted = useRef(false);
useEffect(() => {
if(!mounted.current) {
fetchSomething();
mounted.current = true;
}
return () => { mounted.current = false }
}, [fetchSomething]);
And
const fetchSomething = useCallback(async () => {
...
}, [setData, setPage, data, page]);
fetchSomething is not a dependency here. You don't want to retrigger the effect, you only cause it once when the component mounts. Thats what useEffect(() => ..., []) is for.
I'm trying to implement a data stream that has to use inner observables, where I use one from mergeMap, concatMap etc.
e.g.:
const output$$ = input$$.pipe(
mergeMap(str => of(str).pipe(delay(10))),
share()
);
output$$.subscribe(console.log);
This works fine when logging into console.
But when I try to use it in React like below utilizing useEffect and useState hooks to update some text:
function App() {
const input$ = new Subject<string>();
const input$$ = input$.pipe(share());
const output$$ = input$$.pipe(
mergeMap(str => of(str).pipe(delay(10))),
share()
);
output$$.subscribe(console.log);
// This works
const [input, setInput] = useState("");
const [output, setOutput] = useState("");
useEffect(() => {
const subscription = input$$.subscribe(setInput);
return () => {
subscription.unsubscribe();
};
}, [input$$]);
useEffect(() => {
const subscription = output$$.subscribe(setOutput);
// This doesn't
return () => {
subscription.unsubscribe();
};
}, [output$$]);
return (
<div className="App">
<input
onChange={event => input$.next(event.target.value)}
value={input}
/>
<p>{output}</p>
</div>
);
}
it starts acting weird/unpredictable (e.g.: sometimes the text is updated in the middle of typing, sometimes it doesn't update at all).
Things I have noticed:
If the inner observable completes immediately/is a promise that
resolves immediately, it works fine.
If we print to console instead of useEffect, it works fine.
I believe this has to do something with the inner workings of useEffect and how it captures and notices outside changes, but cannot get it working.
Any help is much appreciated.
Minimal reproduction of the case:
https://codesandbox.io/s/hooks-and-observables-1-7ygd8
I'm not quite sure what you're trying to achieve, but I found a number of problems which hopefully the following code fixes:
function App() {
// Create these observables only once.
const [input$] = useState(() => new Subject<string>());
const [input$$] = useState(() => input$.pipe(share()));
const [output$$] = useState(() => input$$.pipe(
mergeMap(str => of(str).pipe(delay(10))),
share()
));
const [input, setInput] = useState("");
const [output, setOutput] = useState("");
// Create the subscription to input$$ on component mount, not on every render.
useEffect(() => {
const subscription = input$$.subscribe(setInput);
return () => {
subscription.unsubscribe();
};
}, []);
// Create the subscription to output$$ on component mount, not on every render.
useEffect(() => {
const subscription = output$$.subscribe(setOutput);
return () => {
subscription.unsubscribe();
};
}, []);
return (
<div className="App">
<input
onChange={event => input$.next(event.target.value)}
value={input}
/>
<p>{output}</p>
</div>
);
}
I had a similar task but the goal was to pipe and debounce the input test and execute ajax call.
The simple answer that you should init RxJS subject with arrow function in the react hook 'useState' in order to init subject once per init.
Then you should useEffect with empty array [] in order to create a pipe once on component init.
import React, { useEffect, useState } from "react";
import { ajax } from "rxjs/ajax";
import { debounceTime, delay, takeUntil } from "rxjs/operators";
import { Subject } from "rxjs/internal/Subject";
const App = () => {
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
const [filterChangedSubject] = useState(() => {
// Arrow function is used to init Singleton Subject. (in a scope of a current component)
return new Subject<string>();
});
useEffect(() => {
// Effect that will be initialized once on a react component init.
// Define your pipe here.
const subscription = filterChangedSubject
.pipe(debounceTime(200))
.subscribe((filter) => {
if (!filter) {
setLoading(false);
setItems([]);
return;
}
ajax(`https://swapi.dev/api/people?search=${filter}`)
.pipe(
// current running ajax is canceled on filter change.
takeUntil(filterChangedSubject)
)
.subscribe(
(results) => {
// Set items will cause render:
setItems(results.response.results);
},
() => {
setLoading(false);
},
() => {
setLoading(false);
}
);
});
return () => {
// On Component destroy. notify takeUntil to unsubscribe from current running ajax request
filterChangedSubject.next("");
// unsubscribe filter change listener
subscription.unsubscribe();
};
}, []);
const onFilterChange = (e) => {
// Notify subject about the filter change
filterChangedSubject.next(e.target.value);
};
return (
<div>
Cards
{loading && <div>Loading...</div>}
<input onChange={onFilterChange}></input>
{items && items.map((item, index) => <div key={index}>{item.name}</div>)}
</div>
);
};
export default App;