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] }),
Related
I have this component:
import "./styles.css";
import React, { useEffect, useRef, useState } from "react";
import JSONEditor from "jsoneditor";
import "jsoneditor/dist/jsoneditor.css";
const JSONReact = ({ json, mode, onChange }) => {
const ref1 = useRef(null);
const ref2 = useRef(null);
useEffect(() => {
const props = {
onChangeText: (value) => {
console.log(value, "vv");
onChange(value);
},
modes: ["code"]
};
ref1.current = new JSONEditor(ref2.current, props);
if (json) {
ref1.current.set(json);
}
return () => {
ref1.current.destroy();
};
}, []);
useEffect(() => {
if (json) {
ref1?.current.update(json);
}
}, [json]);
return <div ref={ref2} />;
};
export default function App() {
const [state, setState] = useState('{"cars": "22w-08w-23"}');
const onChange = (j) => {
console.log(j);
setState(j);
};
return (
<div className="App">
<JSONReact mode="code" onChange={onChange} json={JSON.parse(state)} />
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
</div>
);
}
When i type something inside editor i get undefined in console.log(j);, but i don't understand why. Who can help to fix it?
https://codesandbox.io/s/admiring-khorana-pdem1l?file=/src/App.js:909-929
Well it is expected as in the docs it clearly says "This callback does not pass the changed contents", over here https://github.com/josdejong/jsoneditor/blob/master/docs/api.md#:~:text=This%20callback%20does%20not%20pass%20the%20changed%20contents
However, There are two ways to do it, that I found from here
https://github.com/josdejong/jsoneditor/blob/master/docs/api.md#configuration-options
First Way: use getText() from JSONEditor object to get the current value, since it will return you string so just parse it in json and pass inside your callback (onChange) prop.
JavaScript
const props = {
onChange: () => {
onChange(JSON.parse(ref1.current.getText()));
},
modes: ["code"]
};
ref1.current = new JSONEditor(ref2.current, props);
Second way: just use onChangeText and it will give the string in the callback,
const props = {
onChangeText: (value) => {
onChange(JSON.parse(value));
},
modes: ["code"]
};
ref1.current = new JSONEditor(ref2.current, props);
EDIT 1: added JSON validation check before calling onChange Props
const isValidJSON = (jsonString) => {
try {
JSON.parse(jsonString);
} catch (e) {
return false;
}
return true;
};
useEffect(() => {
const props = {
onChangeText: (value) => {
isValidJSON(value) && onChange(JSON.parse(value));
},
modes: ["code"]
};
ref1.current = new JSONEditor(ref2.current, props);
EDIT 2: adding error validation using validate(), here in this case we can remove isValidJSON(value) check from below code since we are already checking for JSON errors using validate().
onChangeText: (value) => {
const errors = ref1.current.validate();
errors.then((err) => {
if (!err.length) {
isValidJSON(value) && onChange(JSON.parse(value));
} else {
console.log(err);
}
});
}
I think it is a normal behaviour of JSONEditor, according to docs:
{function} onChange()
Set a callback function triggered when the contents of the JSONEditor
change. This callback does not pass the changed contents, use get() or
getText() for that. Note that get() can throw an exception in mode
text, code, or preview, when the editor contains invalid JSON. Will
only be triggered on changes made by the user, not in case of
programmatic changes via the functions set, setText, update, or
updateText. See also callback functions onChangeJSON(json) and
onChangeText(jsonString). `
The object of this app is to allow input text and URLs to be saved to localStorage. It is working properly, however, there is a lot of repeat code.
For example, localStoredValues and URLStoredVAlues both getItem from localStorage. localStoredValues gets previous input values from localStorage whereas URLStoredVAlues gets previous URLs from localStorage.
updateLocalArray and updateURLArray use spread operator to iterate of previous values and store new values.
I would like to make the code more "DRY" and wanted suggestions.
/*global chrome*/
import {useState} from 'react';
import List from './components/List'
import { SaveBtn, DeleteBtn, DisplayBtn, TabBtn} from "./components/Buttons"
function App() {
const [myLeads, setMyLeads] = useState([]);
const [leadValue, setLeadValue] = useState({
inputVal: "",
});
//these items are used for the state of localStorage
const [display, setDisplay] = useState(false);
const localStoredValues = JSON.parse(
localStorage.getItem("localValue") || "[]"
)
let updateLocalArray = [...localStoredValues, leadValue.inputVal]
//this item is used for the state of localStorage for URLS
const URLStoredVAlues = JSON.parse(localStorage.getItem("URLValue") || "[]")
const tabBtn = () => {
chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
const url = tabs[0].url;
setMyLeads((prev) => [...prev, url]);
// update state of localStorage
let updateURLArray = [...URLStoredVAlues, url];
localStorage.setItem("URLValue", JSON.stringify(updateURLArray));
});
setDisplay(false)
};
//handles change of input value
const handleChange = (event) => {
const { name, value } = event.target;
setLeadValue((prev) => {
return {
...prev,
[name]: value,
};
});
};
const saveBtn = () => {
setMyLeads((prev) => [...prev, leadValue.inputVal]);
setDisplay(false);
// update state of localStorage
localStorage.setItem("localValue", JSON.stringify(updateLocalArray))
};
const displayBtn = () => {
setDisplay(true);
};
const deleteBtn = () => {
window.localStorage.clear();
setMyLeads([]);
};
const listItem = myLeads.map((led) => {
return <List key={led} val={led} />;
});
//interates through localStorage items returns each as undordered list item
const displayLocalItems = localStoredValues.map((item) => {
return <List key={item} val={item} />;
});
const displayTabUrls = URLStoredVAlues.map((url) => {
return <List key={url} val={url} />;
});
return (
<main>
<input
name="inputVal"
value={leadValue.inputVal}
type="text"
onChange={handleChange}
required
/>
<SaveBtn saveBtn={saveBtn} />
<TabBtn tabBtn={tabBtn} />
<DisplayBtn displayBtn={displayBtn} />
<DeleteBtn deleteBtn={deleteBtn} />
<ul>{listItem}</ul>
{/* displays === true show localstorage items in unordered list
else hide localstorage items */}
{display && (
<ul>
{displayLocalItems}
{displayTabUrls}
</ul>
)}
</main>
);
}
export default App
Those keys could be declared as const and reused, instead of passing strings around:
const LOCAL_VALUE = "localValue";
const URL_VALUE = "URLValue";
You could create a utility function that retrieves from local storage, returns the default array if missing, and parses the JSON:
function getLocalValue(key) {
return JSON.parse(localStorage.getItem(key) || "[]")
};
And then would use it instead of repeating the logic when retrieving "localValue" and "URLValue":
const localStoredValues = getLocalValue(LOCAL_VALUE)
//this item is used for the state of localStorage for URLS
const URLStoredVAlues = getLocalValue(URL_VALUE)
Similarly, with the setter logic:
function setLocalValue(key, value) {
localStorage.setItem(key, JSON.stringify(value))
}
and then use it:
// update state of localStorage
let updateURLArray = [...URLStoredVAlues, url];
setLocalValue(URL_VALUE, updateURLArray);
// update state of localStorage
setLocalValue(LOCAL_VALUE, updateLocalArray)
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.
I'm building a custom hook to manage the data coming from a fetch/axios/promise/whateveryouwant function, this hooks allow me to update the data whenever I want using an update function that I return at the end of my hook.
So for now I have two states inside my hooks:
const [data, setData] = useState<T>(defaultValue); // the data coming from the fetch, T is defined by the user of the hook
const [query, setQuery] = useState<Array<{ key: string; value: string }>>([]); // array of query option for the URL
I wanted to implement a dynamic way to add query string to the URL, so I used the query state to have an array of object representing what I wanted but I cannot access the value of the query inside of the update function when calling from outside. Here is the hooks, I highlited the important part :
export default function useFetchV2<T>(
defaultValue: T,
getData: (queryString?: string) => Promise<T>
): {
update: () => void;
data: T;
updateQueryValue: (key: string, value: string) => void; // update, data, updateQueryValue are the return value of the hook.
} {
const [data, setData] = useState<T>(defaultValue);
const [query, setQuery] = useState<Array<{ key: string; value: string }>>([]); // THE STATE I WANT TO ACCESS
console.log(query, data); // Here the state is updating well and everything seems fine
const update = useCallback((): void => {
let queryString;
console.log(query, data);
// But here is the problem, query and data are both at their default value. The state inside the hooks is correct but not here.
if (query.length > 0) { // query.length always at 0
queryString = _.reduce(
query,
(acc, el) => {
return `${acc}${el.key}=${el.value.toString()}`;
},
'?'
);
console.log(queryString);
}
getData(queryString).then(res => setData(res));
}, [data, getData, query]);
const updateQueryValue = useCallback(
(key: string, value: string): void => {
const index = query.findIndex(el => el.key === key);
if (index !== -1) {
if (!value) {
const toto = [...query];
_.pullAt(toto, index);
setQuery(toto);
} else {
setQuery(prev =>
prev.map(el => {
if (el.key === key) {
return { key, value };
}
return el;
})
);
}
} else {
console.log(key, value); // everything is logging well here, good key and value
setQuery([...query, { key, value }]); // setQuery correctly update the state
}
update();
},
[query, update]
);
useEffect(() => {
update();
}, []);
return { update, data, updateQueryValue };
}
It might be the way I'm exporting the function, I'm still not used to the scope.
I called updateQueryValue from a component. The function is called, state is changed, but the update function can't see the difference.
The code is calling setQuery and then immediately calling update and expecting query to be updated value, but it will always be the current value of query. Getting that to happen is the use case of useEffect.
} else {
console.log(key, value); // everything is logging well here, good key and value
setQuery([...query, { key, value }]); // setQuery correctly update the state
}
// This will *always* see the current value of `query`, not the one that
// was just created in the `else` block above.
update();
It sounds like the code should run update after query is updated, which is exactly what useEffect is for.
const updateQueryValue = useCallback(
(key: string, value: string): void => {
const index = query.findIndex(el => el.key === key);
if (index !== -1) {
if (!value) {
const toto = [...query];
_.pullAt(toto, index);
setQuery(toto);
} else {
setQuery(prev =>
prev.map(el => {
if (el.key === key) {
return { key, value };
}
return el;
})
);
}
} else {
setQuery([...query, { key, value }]); // setQuery correctly update the state
}
// This call isn't needed, it will always "see" the old value of
// `query`
// update();
},
[query, update]
);
// Call `update` once when this custom hook is first called as well as
// whenever `query` changes.
useEffect(() => {
update();
}, [query]); // <-- this is new, add `query` as a dependency
useCallback is only needed if something is relying on the identity of the function to make decisions; e.g. if a component receives the function as a prop, it checks whether the prop changes to decide whether to re-render. update in this case does not need a useCallback and might make this hook slightly harder to debug.
function update() {
let queryString;
if (query.length > 0) { // query.length always at 0
queryString = _.reduce(
query,
(acc, el) => {
return `${acc}${el.key}=${el.value.toString()}`;
},
'?'
);
console.log(queryString);
}
getData(queryString).then(res => setData(res));
});
I have a function to update the state and call another function to
update object value in the setState callback method.
I also added a debugger on the breakpoint for the setState callback
method, what I observe is that the value always is the old one.
updateContactPath(path, index) {
const { contactPaths } = this.state;
const { setFieldValue } = this.props;
contactPaths[index] = path;
this.setState(
{
contactPaths,
},
() => setFieldValue('contactPaths', contactPaths),
);
}
We can do something like this to ensure updated state -:
updateContactPath(path, index) {
const { contactPaths } = this.state;
const { setFieldValue } = this.props;
this.setState(
{
[...contactPaths, [index]: path],
},
() => setFieldValue('contactPaths', this.state.contactPaths),
);
}