I have data between sessions that is saved in window.localStorage. When a new session starts a plugin will grab the data and add it to the store.
// ./store/data.js
export const state = () => ({
data: []
})
export const mutations = {
addItemToData (state, item) {
state.data = state.data.push(item)
},
setData (state, data) {
state.data = data
},
}
// ./store/index.js
import localStorage from '../plugins/localStorage'
export const plugins = [localStorage]
// plugins/localStorage.js
const localStorage = store => {
store.subscribe((mutation, state) => {
if (mutation.type === 'data/addItemToData') {
console.log('saving added item to storage')
window.localStorage.setItem('data', JSON.stringify(state.data.data))
}
})
// called when the store is initialized
if (typeof window !== 'undefined') {
if (window.localStorage.data) {
store.commit('data/setData', JSON.parse(window.localStorage.getItem('data')))
}
}
}
export default localStorage
I've thrown all sorts of console statements in, and they all seem to output what they should. The data is in the localStorage, the mutations are firing, but after all that the data isn't in the store.
Any help would be great, thanks!
several things that do not make sense and that you can resolve
#1
addItemToData (state, item) {
state.data = state.data.push(item)
}
Array.push() does not return anything, so it should be written as
addItemToData (state, item) {
state.data.push(item)
}
#2
in your localStorage.js file, when you initialize the data storage, you are assuming that a data variable exists:
if (window.localStorage.data) { ...
but that will never exist from the code you show, as you are adding the data to another variable
window.localStorage.setItem('cart', ...
either change cart into data or data into cart
#3
If still does not work, I would suspect that your plugin is running before the store is actually initialized, for that, make sure you wait a moment before attempt to fill the store with the localStorage data
something like
...
if (window.localStorage.data) {
setTimeout(() => {
store.commit('data/setData', JSON.parse(window.localStorage.getItem('data')))
}, 500)
}
Working example is available on Netlify and code on GitHub
you can use vuex-persistedstate for saving in localstorage
Array.push Returns the length of the Array after insertion, In your case, it overrides the data Array to Number when you make a mutation. Do like the following..
export const state = () => ({
data: []
})
export const mutations = {
addItemToData (state, item) {
state.data.push(item)
// OR
state.data = [...state.data, item]
},
setData (state, data) {
state.data = data
},
}
And also make sure, you have data in localStorage.
try { // handles if any parsing errors
const data = JSON.parse(window.localStorage.getItem('data')
store.commit('data/setData', data)
} catch(err) {
console.log('JSON parsing error', err)
}
Related
Im mostly using SWR to get data, however I have a situation that I need to update data. The problem is, I need an indicator that this request is ongoing, something like isLoading flag. In the docs there's a suggestion to use
const isLoading = !data && !error;
But of course when updating (mutating) the data still exists so this flag is always false. The same with isValidating flag:
const { isValidating } = useSWR(...);
This flag does NOT change when mutation is ongoing but only when its done and GET request has started.
Question
Is there a way to know if my PUT is loading? Note: I dont want to use any fields in state because it won't be shared just like SWR data is. Maybe Im doing something wrong with my SWR code?
const fetcher = (url, payload) => axios.post(url, payload).then((res) => res);
// ^^^^^ its POST but it only fetches data
const updater = (url, payload) => axios.put(url, payload).then((res) => res);
// ^^^^^ this one UPDATES the data
const useHook = () => {
const { data, error, mutate, isValidating } = useSWR([getURL, payload], fetcher);
const { mutate: update } = useSWRConfig();
const updateData = () => {
update(getURL, updater(putURL, payload)); // update data
mutate(); // refetch data after update
};
return {
data,
updateData,
isValidating, // true only when fetching data
isLoading: !data && !error, // true only when fetching data
}
Edit: for any other who reading this and facing the same issue... didnt find any solution for it so switched to react-query. Bye SWR
const { mutate: update } = useSWRConfig();
const updateData = () => {
// this will return promise
update(getURL, updater(putURL, payload)); // update data
mutate(); // refetch data after update
};
By using react-toastify npm module to show the user status.
// first wrap your app with: import { ToastContainer } from "react-toastify";
import { toast } from "react-toastify";
const promise=update(getURL, updater(putURL, payload))
await toast.promise(promise, {
pending: "Mutating data",
success: "muttation is successfull",
error: "Mutation failed",
});
const markSourceMiddleware = (useSWRNext) => (key, fetcher, config) => {
const nextFetcher = (...params) =>
fetcher(...params).then((response) => ({
source: "query",
response,
}));
const swr = useSWRNext(key, nextFetcher, config);
return swr;
};
const useHook = () => {
const {
data: { source, response },
mutate,
} = useSWR(key, fetcher, { use: [markSourceMiddleware] });
const update = mutate(
updateRequest().then((res) => ({
source: "update",
response,
})),
{
optimisticData: {
source: "update",
response,
},
}
);
return {
update,
updating: source === "update",
};
};
Hmm based on that:
https://swr.vercel.app/docs/conditional-fetching
It should work that the "is loading" state is when your updater is evaluates to "falsy" value.
REMAINDER! I don't know react swr just looked into docs - to much time at the end of the weekend :D
At least I hope I'll start discussion :D
I'm currently building a simple chat feature in my app using redux saga. Chat successfully entered from the emit server to the client
But the problem is, so when I move from the chat page and then back to the chat page, the last chat data sent will be duplicated to 2. Then when I move again and return to the chat page, the sent chat will be 3 and so on.
i think this happened maybe because the listener loop on fetchKonsultasi() function was stacked
Here is a dispatch function onGetKonsultasi() to listen to emits and onGetListKonsultasi() to retrieve chat list data from the server. Chat data is stored in the dataListKonsultasi, when there is an emit from the server, the dataListKonsultasi state will be updated
Someone help me, I really appreciate your help
index.js
useEffect(() => {
onGetListKonsultasi()
onGetKonsultasi()
}, [])
------
Konsultasi.propTypes = {
dataKonsultasi: PropTypes.object,
dataListKonsultasi: PropTypes.object,
onGetKonsultasi: PropTypes.func,
onGetListKonsultasi: PropTypes.func,
}
const mapStateToProps = ({ konsultasiReducer }) => ({
dataKonsultasi: konsultasiReducer.dataKonsultasi,
dataListKonsultasi: konsultasiReducer.dataListKonsultasi,
})
const mapDispatchToProps = dispatch => ({
onGetKonsultasi: () => dispatch(getKonsultasi()),
onGetListKonsultasi: () => dispatch(getListKonsultasi()),
})
export default connect(mapStateToProps, mapDispatchToProps)(Konsultasi)
saga.js
function createSocketConnection(url) {
return io(url)
}
function createSocketChannel(socket) {
return eventChannel(emit => {
const eventHandler = event => {
emit(event)
}
const errorHandler = errorEvent => {
emit(new Error(errorEvent.reason))
}
socket.on("AdminReceiveMessage", eventHandler)
socket.on("error", errorHandler)
const unsubscribe = () => {
socket.off("AdminReceiveMessage", eventHandler)
}
return unsubscribe
})
}
function* fetchKonsultasi() {
const socket = yield call(createSocketConnection, env.SOCKET_URL)
const socketChannel = yield call(createSocketChannel, socket)
while (true) {
try {
const response = yield take(socketChannel)
yield put(updateListKonsultasiSuccess(response))
} catch (err) {
console.log("socket error: ", err)
}
}
}
function* fetchListKonsultasi() {
try {
const response = yield call(getListKonsultasi)
yield put(getListKonsultasiSuccess(response))
} catch (error) {
yield put(getListKonsultasiFail(error))
}
}
export function* watchSocket() {
yield takeEvery(GET_KONSULTASI, fetchKonsultasi)
yield takeEvery(GET_LIST_KONSULTASI, fetchListKonsultasi)
}
function* konsultasiSaga() {
yield all([fork(watchSocket)])
}
reducer.js
case GET_LIST_KONSULTASI_SUCCESS:
return {
...state,
dataListKonsultasi: action.payload,
}
case UPDATE_LIST_KONSULTASI_SUCCESS:
return {
...state,
dataListKonsultasi: {
users: { ...state.dataListKonsultasi.users },
data: [...state.dataListKonsultasi.data, action.payload],
},
dataListKonsultasi Structure :
{
"users": {
......
},
"data": [
{
..chat_data...
},
{
..chat_data...
},
]
}
acutely the problem come from when you change tab with your router you didn't lost your data that's you add into your store in socket connection, then each time you come into your page you get new data and combine last data with new data and that's happening.
good way to solve this problem is, create an action to clear data list from store and call this action inside of closing socket connection, by this approach every time you lose connection with your socket you clear data and re write data with connecting again.
case CLEAR_LIST:
return {
...state,
dataListKonsultasi: {
users: {},
data: [],
},
}
and then
socket.on("disconnect", () => {
// call your clear action here
});
Hey there Muhammand so over here is where I think might help you out
case UPDATE_LIST_KONSULTASI_SUCCESS:
return {
...state,
dataListKonsultasi: {
users: { ...state.dataListKonsultasi.users },
//Here you are always spreading the messages in the store state and
//Then what is happening here is you are adding all the data fetched
// previous data + action.payload from the server so every time you load
//the messages page youre going to be adding a new batch of messages to
//the store state
data: [...state.dataListKonsultasi.data, action.payload],
},
I think try creating a action for clearing your messages data every time you unmount the messages page from the store then whenever you come back to the messages page it should be good Let me know if this helps :D
e.g:
case CLEAR_DATA_LIST: {
return {
...state,
dataListKonsultasi: {
...state.dataListKonsultasi,
data: [],
},
Then either call this when unmounting or in your socket.disconnect and if you want to clear the users data too just handle that according to what you want to acheive :D Might be cool to keep the users data and not clear it then you can always show the amount of users online etc
I get todo list with useQuery.
const { data, refetch } = useQuery(GET_TODOS);
After creating a todo, I get todo list with refetch like below.
const [ addTodo ] = useMutation(ADD_TODO, {
onComplete: () => refetch()
});
const handleAddTodo = useCallback((todoArgs) => {
addTodo({ variables: todoArgs });
}, []);
But It is obviously wasted time.
I tried to update only in an updated part. for that, I saved todos into a state and I changed this.
const [todos, setTodos] = useState([]);
...
const [ addTodo ] = useMutation(ADD_TODO, {
onComplete: (updatedData) => {
setTodos((prevTodos) => {
const newTodos = prevTodos.map((todo) => todo.id === updatedData.id ? updatedData : todo);
return newTodos;
});
}
}
...
useEffect(() => {
setTodos(data);
}, [data]);
...
But I'm not sure It is a right way. I think there may be an official way for updating a part of data.
What's the best way to fetch a partial data after Creating, Updating, Deleting?
I'm using 'no-cache' as a default option in the project.
Managing the query response in a new state seems a bit overkill to me.
In fact, Apollo GraphQL client automatically refetch the updated data, as long as you are returning the updated data id field in the mutation result.
For other cases, you may want to use a custom update function option.
You can read more about that here:
https://www.apollographql.com/blog/apollo-client/caching/when-to-use-refetch-queries/
Problem
The event to send data to renderer is triggered on a file change with chokidar. When the file watcher is triggered and the process sends the event to the renderer.
My problem is that when the EmitterEvent is triggered I input the current state of my useState() variable but only the initial state is passed to my function.
Edited: my problem is that I can not pass the updated data variable to the updateData(newData) function which is called from the emitter inside the preload.js.
Question
How can I pass the state variable data to the call ?
Is there a way that I can change my preload.js in order for the api.receive function to return a string in order not to have to pass a function to the emitter ? (please check the updateData(newData) function for more info)
Is there a better way to achieve this ?
This could also help me to initialize the data for the first render.
preload.js
contextBridge.exposeInMainWorld(
"api", {
send: (channel, data) => {
// whitelist channels
let validChannels = ["file", "save"];
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, data);
}
},
receive: (channel, func) => {
let validChannels = ["file", "save"];
if (validChannels.includes(channel)) {
// Deliberately strip event as it includes `sender`
ipcRenderer.on(channel, (event, ...args) => func(...args));
}
},
}
electron.js
function ReadNodesFileToIpc(path) {
fs.readFile(path, (error, data) => {
win.webContents.send("file", data);
});
}
Code in component that receives data
function MyForceGraphComponent(){
const [data, setData] = useState({ nodes: [{ id: 0, otherinfo: [] }], links: [] });
var isDataInittiallized = 0;
...
function updateData (newData, data) {
if (data.nodes.length !== 1){
// do stuff
setData({ nodes: data.nodes, links: data.links });
}else{
if (!isDataInittiallized){
setData({ nodes: newData.nodes, links: newData.links });
isDataInittiallized = 1;
}
}
}
...
useEffect(() => {
...
window.api.receive("file", (bytesArray) => {
var newData = JSON.parse(String.fromCharCode.apply(null, bytesArray));
updateData(newData); // Check function bellow
});
...
}, []);
}
updateData(newData) (a function inside my components fuction)
isDataInittiallized is a variable inside the component that the change got passed to the emitter
data is my variable from the useState() function that the change did NOT got passed to the emitter even though the setData() previously changed the data successfully. So the length remains 1 and it contains the same elements from when it was firstly initialized .
Other info
Have tried to play with passing the data variable to the receive function without any success.
Most probably when the emitter is getting set the function passed (the one that does JSON.parse) is getting passed along and never changed after.
Since the issue is related to stale data inside the updateData function, I suggest to make the following updates:
// Simplified for the sake of brevity
function MyForceGraphComponent() {
const [data, setData] = useState({ nodes: [{ id: 0 }] })
// isDataInitialized needs to be tracked just as any other state
const [isDataInitialized, setIsDataInitialized] = useState(false)
// Wrap `updateData` in `React.useCallback` to prevent stale data
const updateData = useCallback(
(nextData) => {
// skip the update, no need to update the state with existing data
if (isDataInitialized) return;
setData({ nodes: nextData.nodes })
setIsDataInitialized(true) // set to `true` to prevent future updates
},
[isDataInitialized, setIsDataInitialized]
)
const handleReceivedData = useCallback(
(bytesArray) => {
const nextData = JSON.parse(...)
updateData(nextData)
},
[updateData]
)
useEffect(() => {
window.api.receive('file', handleReceivedData);
}, [handleReceivedData])
}
Have a look at this example that mimics what you're trying to do:
CodeSandbox
There some error in you code that i found. When you call updateData(newData), data is null, data.nodes will not work. you can modify you code, "if (data
&& data.nodes && data.nodes.length !== 1)"
So I have a situation where I have this component that shows a user list. First time the component loads it gives a list of all users with some data. After this based on some interaction with the component I get an updated list of users with some extra attributes. The thing is that all subsequent responses only bring back the users that have these extra attributes. So what I need is to save an initial state of users that has a list of all users and on any subsequent changes keep updating/adding to this state without having to replace the whole state with the new one because I don't want to lose the list of users.
So far what I had done was that I set the state in Redux on that first render with a condition:
useEffect(() => {
if(users === undefined) {
setUsers(userDataFromApi)
}
userList = users || usersFromProp
})
The above was working fine as it always saved the users sent the first time in the a prop and always gave priority to it. Now my problem is that I'm want to add attributes to the list of those users in the state but not matter what I do, my component keeps going into an infinite loop and crashing the app. I do know the reason this is happening but not sure how to solve it. Below is what I am trying to achieve that throws me into an infinite loop.
useEffect(() => {
if(users === undefined) {
setUsers(userDataFromApi)
} else {
//Users already exist in state
const mergedUserData = userDataFromApi.map(existingUser => {
const matchedUser = userDataFromApi.find(user => user.name === existingUser.name);
if (matchedUser) {
existingUser.stats = user.stats;
}
return existingUser;
})
setUsers(mergedUserData)
}
}, [users, setUsers, userDataFromApi])
So far I have tried to wrap the code in else block in a separate function of its own and then called it from within useEffect. I have also tried to extract all that logic into a separate function and wrapped with useCallback but still no luck. Just because of all those dependencies I have to add, it keeps going into an infinite loop. One important thing to mention is that I cannot skip any dependency for useCallback or useEffect as the linter shows warnings for that. I need to keep the logs clean.
Also that setUsers is a dispatch prop. I need to keep that main user list in the Redux store.
Can someone please guide me in the right direction.
Thank you!
Since this is based on an interaction could this not be handled by the the event caused by the interaction?
const reducer = (state, action) => {
switch (action.type) {
case "setUsers":
return {
users: action.payload
};
default:
return state;
}
};
const Example = () => {
const dispatch = useDispatch();
const users = useSelector(state => state.users)
useEffect(() => {
const asyncFunc = async () => {
const apiUsers = await getUsersFromApi();
dispatch({ type: "setUsers", payload: apiUsers });
};
// Load user data from the api and store in Redux.
// Only do this on component load.
asyncFunc();
}, [dispatch]);
const onClick = async () => {
// On interaction (this case a click) get updated users.
const userDataToMerge = await getUpdatedUserData();
// merge users and assign to the store.
if (!users) {
dispatch({ type: "setUsers", payload: userDataToMerge });
return;
}
const mergedUserData = users.map(existingUser => {
const matchedUser = action.payload.find(user => user.name === existingUser.name);
if (matchedUser) {
existingUser.stats = user.stats;
}
return existingUser;
});
dispatch({ type: "setUsers", payload: mergedUserData });
}
return (
<div onClick={onClick}>
This is a placeholder
</div>
);
}
OLD ANSWER (useState)
setUsers can also take a callback function which is provided the current state value as it's first parameter: setUsers(currentValue => newValue);
You should be able to use this to avoid putting users in the dependency array of your useEffect.
Example:
useEffect(() => {
setUsers(currentUsers => {
if(currentUsers === undefined) {
return userDataFromApi;
} else {
//Users already exist in state
const mergedUserData = currentUsers.map(existingUser => {
const matchedUser = userDataFromApi.find(user => user.name === existingUser.name);
if (matchedUser) {
existingUser.stats = user.stats;
}
return existingUser;
});
return mergedUserData;
}
});
}, [setUsers, userDataFromApi]);