React Hooks LocalStorage setState override elements - reactjs

I'm struggling with React hooks using useEffect in case of storing tasks in localstorage, so refreshing page will still handle the elements from the list. The problem is while I'm trying to get the elements from LocalStorage and set them for todos state. They exist inside localStorage, but element that is inside todos state is only one from localstorage.
Here is a Component that handling this:
const [todos, setTodos] = useState([]);
const [todo, setTodo] = useState("");
const addTask = (event) => {
event.preventDefault();
let newTask = {
task: todo,
id: Date.now(),
completed: false,
};
setTodos([...todos, newTask]);
setTodo("");
};
const getLocalStorage = () => {
for (let key in localStorage) {
if (!localStorage.hasOwnProperty(key)) {
continue;
}
let value = localStorage.getItem(key);
try {
value = JSON.parse(value);
setTodos({ [key]: value });
} catch (event) {
setTodos({ [key]: value });
}
}
};
const saveLocalStorage = () => {
for (let key in todos) {
localStorage.setItem(key, JSON.stringify(todos[key]));
}
};
useEffect(() => {
getLocalStorage();
}, []);
useEffect(() => {
saveLocalStorage();
}, [saveLocalStorage]);
const testClick = () => {
console.log(todos);
};
return (
<div className="App">
<h1>What would you like to do?</h1>
<TodoForm
todos={todos}
value={todo}
inputChangeHandler={inputChangeHandler}
addTask={addTask}
removeCompleted={removeCompleted}
/>
{/* <TodoList todos={todos} toogleCompleted={toogleCompleted} /> */}
<button onClick={testClick}>DISPLAY TODOS</button>
</div>
);
Second problematic error while refreshing page:
Apart from that I can not display elements after getting them from local storage. Probably I'm missing some stupid little thing, but I stuck in this moment and I was wondering if small explanation would be possible where I'm making mistake.

I think you are overwriting the state value all together. You want to append to what exists in the todo state. You could use the spread operator to set the todo to everything that's already in there plus the new value.
Something like this:
setTodos({...todos, [key]: value });
setTodos([...todos, [key]: value ]);
Note that spread operator is a shallow clone. If you have a deeper object (more than two layers i think) then use something like cloneDeep from the lodash library.
To see whats happening change this block to add the console.logs
try {
value = JSON.parse(value);
setTodos({ [key]: value });
console.log(todos);
} catch (event) {
console.log('There was an error:', error);
// Should handle the error here rather than try to do what failed again
//setTodos({ [key]: value });
}
Edit: regarding the error you added.
You are trying to use map on an object. map is an array function.
Instead you would convert your object to an array and then back:
const myObjAsArr = Object.entries(todos).map(([k,v]) => {
// Do stuff with key and value
}
const backToObject = Object.fromEntries(myObjAsArr);
Change this to set an array (not object):
setTodos([...todos, [key]: value ]);
Put console logs everywhere so you can see whats happening.

Related

React array is empty after I filled it in Firebase function

I'm trying to load data using React and Firebase.
Unfortunately, I can't get it to display them.
In Firebase's res.items.forEach((itemRef) function, I fill an array.
I would like to access this later. However, this is then empty. How so ?
const array = [];
function loadList() {
const storage = getStorage();
const storageRef = sRef(storage, "images/");
// Find all the prefixes and items.
listAll(storageRef)
.then((res) => {
res.prefixes.forEach((folderRef) => {
// All the prefixes under listRef.
// You may call listAll() recursively on them.
});
res.items.forEach((itemRef) => {
array.push(itemRef._location);
// All the items under listRef.
console.log(itemRef._location);
});
})
.catch((error) => {
// Uh-oh, an error occurred!
});
}
function App() {
loadList();
return (
<div className="App">
<Stack direction="row" alignItems="center" spacing={2}>
// ARRAY IS EMPTY
{array?.map((value, key) => {
return <h1>{value}</h1>;
})}
...
From your explaination, here is what I gathered you are trying to do:
Fetch the documents from cloud firestore.
Store them in an array.
Create a list of Components in the browser view rendered by react using the array that was fetched.
For such, fetching and rendering tasks, your best bet would be using Callbacks, State and Effect.
So:
State would hold the values for react to render.
Effect will fetch the data when the component loads.
Callback will do the actual fetching because asynchronous fetching on useEffect directly is discouraged.
const storage = getStorage();
const listRef = ref(storage, 'files/uid');
function ImageApp()
{
// This is the array state of items that react will eventually render
// It is set to empty initially because we will have to fetch the data
const [items, setItems] = React.useState([]);
React.useEffect(() =>
{
fetchItemsFromFirebase();
}, []); // <- Empty array means this will only run when ImageApp is intially rendered, similar to onComponentMount for class
const fetchItemsFromFirebase = React.useCallback(async () =>
{
await listAll(listRef)
.then((res) =>
{
// If (res.items) is already an array, then use the method below, it would be faster than iterating over every single location
// cosnt values = res.items.map((item) => item._location);
// If res.items doesnt already return an array, then you unfortunately have to add each item individually
const values = [];
for (const itemRef of res.items)
{
values.push(itemRef._location);
}
console.log(values);
setItems(values);
})
.catch((error) => { console.error(error) });
}, []); // <- add "useState" values inside the array if you want the fetch to happen every smth it changes
return (
<div className="App">
{
items && // Makes sure the fragment below will only be rendered if `items` is not undefined
// An empty array is not undefined so this will always return false, but its good to use for the future :)
<>
{
(items.map((item, itemIndex) =>
<h1 key={ itemIndex }>{ item }</h1>
))
}
</>
}
</div>
)
}

react useCallback not updating function

Isn't the hook useCallback supposed to return an updated function every time a dependency change?
I wrote this code sandbox trying to reduce the problem I'm facing in my real app to the minimum reproducible example.
import { useCallback, useState } from "react";
const fields = [
{
name: "first_name",
onSubmitTransformer: (x) => "",
defaultValue: ""
},
{
name: "last_name",
onSubmitTransformer: (x) => x.replace("0", ""),
defaultValue: ""
}
];
export default function App() {
const [instance, setInstance] = useState(
fields.reduce(
(acc, { name, defaultValue }) => ({ ...acc, [name]: defaultValue }),
{}
)
);
const onChange = (name, e) =>
setInstance((instance) => ({ ...instance, [name]: e.target.value }));
const validate = useCallback(() => {
Object.entries(instance).forEach(([k, v]) => {
if (v === "") {
console.log("error while validating", k, "value cannot be empty");
}
});
}, [instance]);
const onSubmit = useCallback(
(e) => {
e.preventDefault();
e.stopPropagation();
setInstance((instance) =>
fields.reduce(
(acc, { name, onSubmitTransformer }) => ({
...acc,
[name]: onSubmitTransformer(acc[name])
}),
instance
)
);
validate();
},
[validate]
);
return (
<div className="App">
<form onSubmit={onSubmit}>
{fields.map(({ name }) => (
<input
key={`field_${name}`}
placeholder={name}
value={instance[name]}
onChange={(e) => onChange(name, e)}
/>
))}
<button type="submit">Create object</button>
</form>
</div>
);
}
This is my code. Basically it renders a form based on fields. Fields is a list of objects containing characteristics of the field. Among characteristic there one called onSubmitTransformer that is applied when user submit the form. When user submit the form after tranforming values, a validation is performed. I wrapped validate inside a useCallback hook because it uses instance value that is changed right before by transform function.
To test the code sandbox example please type something is first_name input field and submit.
Expected behaviour would be to see in the console the error log statement for first_name as transformer is going to change it to ''.
Problem is validate seems to not update properly.
This seems like an issue with understanding how React lifecycle works. Calling setInstance will not update instance immediately, instead instance will be updated on the next render. Similarly, validate will not update until the next render. So within your onSubmit function, you trigger a rerender by calling setInstance, but then run validate using the value of instance at the beginning of this render (before the onSubmitTransformer functions have run).
A simple way to fix this is to refactor validate so that it accepts a value for instance instead of using the one from state directly. Then transform the values on instance outside of setInstance.
Here's an example:
function App() {
// setup
const validate = useCallback((instance) => {
// validate as usual
}, []);
const onSubmit = useCallback((e) => {
e.preventDefault();
e.stopPropagation();
const transformedInstance = fields.reduce((acc, {name, onSubmitTransformer}) => ({
...acc,
[name]: onSubmitTransformer(acc[name]),
}), instance);
setInstance(transformedInstance);
validate(transformedInstance);
}, [instance, validate]);
// rest of component
}
Now the only worry might be using a stale version of instance (which could happen if instance is updated and onSubmit is called in the same render). If you're concerned about this, you could add a ref value for instance and use that for submission and validation. This way would be a bit closer to your current code.
Here's an alternate example using that approach:
function App() {
const [instance, setInstance] = useState(/* ... */);
const instanceRef = useRef(instance);
useEffect(() => {
instanceRef.current = instance;
}, [instance]);
const validate = useCallback(() => {
Object.entries(instanceRef.current).forEach(([k, v]) => {
if (v === "") {
console.log("error while validating", k, "value cannot be empty");
}
});
}, []);
const onSubmit = useCallback((e) => {
e.preventDefault();
e.stopPropagation();
const transformedInstance = fields.reduce((acc, {name, onSubmitTransformer}) => ({
...acc,
[name]: onSubmitTransformer(acc[name]),
}), instanceRef.current);
setInstance(transformedInstance);
validate(transformedInstance);
}, [validate]);
// rest of component
}

How to use useEffect correctly to apply modifications to the array state using Firestore?

I'm using React firebase to make a Slack like chat app. I am listening to the change of the state inside the useEffect on rendering. (dependency is []).
The problem I have here is, how to fire the changes when onSnapshot listener splits out the changed state. If change.type is "modified", I use modifyCandidate (which is an interim state) to save what's been updated, and hook this state in the second useEffect.
The problem of second effect is, without dependency of chats, which is the array of chat, there is no chat in chats (which is obviously true in the initial rendering). To get chats, I add another dependency to second effect. Now, other problem I get is whenever I face changes or addition to the database, the second effect is fired even if modification didn't take place.
How can I effectively execute second effect when only modification occurs as well as being able to track the changes of chats(from the beginning) or
am I doing something awkward in the listening phase?
Please share your thoughts! (:
useEffect(() => {
const chatRef = db.collection('chat').doc('room_' + channelId).collection('messages')
chatRef.orderBy("created").onSnapshot((snapshot) => {
snapshot.docChanges().forEach((change) => {
if (change.type === "added") {
console.log("New message: ", change.doc.data());
}
if (change.type === "modified") {
console.log("Modified message: ", change.doc.data());
setModifyCandidate(change.doc.data());
}
if (change.type === "removed") {
console.log("remove message: ", change.doc.data());
}
});
});
}, [])
useEffect(() => {
if(!modifyCandidate){
return
}
const copied = [...chats];
const index = copied.findIndex(chat => chat.id === modifyCandidate.id)
copied[index] = modifyCandidate
setChats(copied)
}, [modifyCandidate, chats])
initially, I also use this useEffect to load chats.
useEffect(() => {
const chatRef = db.collection('chat').doc('room_' + channelId).collection('messages')
chatRef.orderBy("created").get().then((snapshot) => {
const data = snapshot.docs.map((doc) => ({
id: doc.id,
...doc.data(),
}));
setChats(data);
})
}, [])
return <>
{
chats.map((chat) => {
return <div key={chat.id}>
<ChatCard chat={chat} users={users} uid={uid} index={chat.id} onEmojiClick={onEmojiClick}/>
</div>
})
}
</>
use useMemo instead of 2nd useEffect.
const chat = useMemo(() => {
if(!modifyCandidate){
return null
}
const copied = [...chats];
const index = copied.findIndex(chat => chat.id === modifyCandidate.id)
copied[index] = modifyCandidate
return copied
}, [modifyCandidate])

React Hooks createContext: How to update UserContext when its an array?

I have state in my UserContext component:
var initialState = {
avatar: '/static/uploads/profile-avatars/placeholder.jpg',
markers: []
};
var UserContext = React.createContext();
function setLocalStorage(key, value) {
try {
window.localStorage.setItem(key, JSON.stringify(value));
} catch (errors) {
// catch possible errors:
console.log(errors);
}
}
function getLocalStorage(key, initialValue) {
try {
const value = window.localStorage.getItem(key);
return value ? JSON.parse(value) : initialValue;
} catch (e) {
// if error, return initial value
return initialValue;
}
}
function UserProvider({ children }) {
const [user, setUser] = useState(() => getLocalStorage('user', initialState));
const [isAvatarUploading, setIsAvatarUploading] = useState(true);
And returning a function to update the array:
return (
<UserContext.Provider
value={{
userMarkers: user.markers,
setUserMarkers: markers => setUser({ ...user, markers }),
}}
>
{children}
</UserContext.Provider>
);
In my consuming function I am trying to use that function to add an object to the initial array i.e. markers.
export default function MyMap() {
var { userMarkers, setUserMarkers } = useContext(UserContext);
imagine there is an operation above which changes this value:
setUserMarkers({"id":1,"lat":40.73977053760343,"lng":-73.55357676744462});
What is happening is the old object get completely overridden. so the array gets updated with a new object. Not added. Please help. I also tried concat and it didn't work?
setUserMarkers: markers => setUser(()=> user.markers.concat(markers)),
So markers should be:
markers: [{"id":0,"lat":40.73977033730345,"lng":-66.55357676744462},{"id":1,"lat":40.73977053760343,"lng":-73.55357676744462}]
Call your function like this to take the previous value and append to it.
setUserMarkers([...userMarkers, {"id":1,"lat":40.73977053760343,"lng":-73.55357676744462}])
You could also update your function call to always append if you never need to overwrite values in it. Then you can just call it with the new value.
setUserMarkers: markers => setUser({ ...user, markers: [...user.markers, markers] }),

React hooks : how to watch changes in a JS class object?

I'm quite new to React and I don't always understand when I have to use hooks and when I don't need them.
What I understand is that you can get/set a state by using
const [myState, setMyState] = React.useState(myStateValue);
So. My component runs some functions based on the url prop :
const playlist = new PlaylistObj();
React.useEffect(() => {
playlist.loadUrl(props.url).then(function(){
console.log("LOADED!");
})
}, [props.url]);
Inside my PlaylistObj class, I have an async function loadUrl(url) that
sets the apiLoading property of the playlist to true
gets content
sets the apiLoading property of the playlist to false
Now, I want to use that value in my React component, so I can set its classes (i'm using classnames) :
<div
className={classNames({
'api-loading': playlist.apiLoading
})}
>
But it doesn't work; the class is not updated, even if i DO get the "LOADED!" message in the console.
It seems that the playlist object is not "watched" by React. Maybe I should use react state here, but how ?
I tested
const [playlist, setPlaylist] = React.useState(new PlaylistObj());
React.useEffect(() => {
//refresh playlist if its URL is updated
playlist.loadUrl(props.playlistUrl).then(function(){
console.log("LOADED!");
})
}, [props.playlistUrl]);
And this, but it seems more and more unlogical to me, and, well, does not work.
const [playlist, setPlaylist] = React.useState(new PlaylistObj());
React.useEffect(() => {
playlist.loadUrl(props.playlistUrl).then(function(){
console.log("LOADED!");
setPlaylist(playlist); //added this
})
}, [props.playlistUrl]);
I just want my component be up-to-date with the playlist object. How should I handle this ?
I feel like I'm missing something.
Thanks a lot!
I think you are close, but basically this issue is you are not actually updating a state reference to trigger another rerender with the correct loading value.
const [playlist, setPlaylist] = React.useState(new PlaylistObj());
React.useEffect(() => {
playlist.loadUrl(props.playlistUrl).then(function(){
setPlaylist(playlist); // <-- this playlist reference doesn't change
})
}, [props.playlistUrl]);
I think you should introduce a second isLoading state to your component. When the effect is triggered whtn the URL updates, start by setting loading true, and when the Promise resolves update it back to false.
const [playlist] = React.useState(new PlaylistObj());
const [isloading, setIsLoading] = React.useState(false);
React.useEffect(() => {
setIsLoading(true);
playlist.loadUrl(props.playlistUrl).then(function(){
console.log("LOADED!");
setIsLoading(false);
});
}, [props.playlistUrl]);
Use the isLoading state in the render
<div
className={classNames({
'api-loading': isLoading,
})}
>
I also suggest using the finally block of a Promise chain to end the loading in the case that the Promise is rejected your UI doesn't get stuck in the loading "state".
React.useEffect(() => {
setIsLoading(true);
playlist.loadUrl(props.playlistUrl)
.then(function() {
console.log("LOADED!");
})
.finally(() => setIsLoading(false));
}, [props.playlistUrl]);
Here you go:
import React from "react";
class PlaylistAPI {
constructor(data = []) {
this.data = data;
this.listeners = [];
}
addListener(fn) {
this.listeners.push(fn);
}
removeEventListener(fn) {
this.listeners = this.listeners.filter(prevFn => prevFn !== fn)
}
setPlayList(data) {
this.data = data;
this.notif();
}
loadUrl(url) {
console.log("called loadUrl", url, this.data)
}
notif() {
this.listeners.forEach(fn => fn());
}
}
export default function App() {
const API = React.useMemo(() => new PlaylistAPI(), []);
React.useEffect(() => {
API.addListener(loadPlaylist);
/**
* Update your playlist and when user job has done, listerners will be called
*/
setTimeout(() => {
API.setPlayList([1,2,3])
}, 3000)
return () => {
API.removeEventListener(loadPlaylist);
}
}, [API])
function loadPlaylist() {
API.loadUrl("my url");
}
return (
<div className="App">
<h1>Watching an object by React Hooks</h1>
</div>
);
}
Demo in Codesandbox

Resources