I am using react-redux to store a state modeData which is an array of objects. Using react-hotkeys-hook, when the user presses ctrl+l, a function runs which updates the state. In that same function, when I console.log the same state, it does not reflect the changes.
Here's the code I'm using:
import { useSelector, useDispatch } from "react-redux";
import { modeDataIncrement } from "./redux/modeSlice";
import { useHotkeys } from "react-hotkeys-hook";
import { useEffect } from "react";
//..
const modeData = useSelector((state) => state.mode.modeData); //array of objects.
const handler = (id) => {
const isActive = modeData.find((x) => x.id === id).active;
console.log(modeData); // does not show the updated state
dispatch(modeDataIncrement(id));
};
useHotkeys("ctrl+l", () => handler("Clone"));
useEffect(() => {
console.log(modeData); //working fine!
}, [modeData]);
modeSlice.js:
import { createSlice } from "#reduxjs/toolkit";
export const modeSlice = createSlice({
name: "mode",
initialState: {
modeData: [{ id: "Clone", active: false }],
},
reducers: {
modeDataIncrement: (state, action) => {
state.modeData.find(
(e) => e.id === action.payload && (e.active = !e.active)
);
},
},
});
export const { modeDataIncrement } = modeSlice.actions;
export default modeSlice.reducer;
Any thoughts on what I'm doing wrong?
Thanks!
Your code needs to use the latest version of modeData, but based on the source code of useHotkeys, it will memoize the function, and not update it automatically. So you're always using the version of the callback that existed on the first render.
To fix this, you need to pass a dependency array in to useHotkeys, so it can break the memoization:
const handler = (id) => {
const isActive = modeData.find((x) => x.id === id).active;
console.log(modeData);
dispatch(modeDataIncrement(id));
};
useHotkeys("ctrl+l", () => handler("Clone"), [modeData]);
Related
I am trying redux-thunk for the first time Hence working on a simple project the thunk uses the API and displays the data on the screen but the API is returning a JSON object ,to display the titles on the screen I need to use the .map() function to map through the object, but the object doesn't allow us to use map() function so I need to convert the JSON data to an array and the use .map() function to achieve the desired result but I don't know how to convert the JSON data to an array
I tried different approaches to deal with this but nothing seems to work for me Here is what I need
const codeReturnedFromJSONRequest ={data:{0:somedata}} //example JOSN data
what I want my code to look like :
const requiredData=[{data:{0:somedata}}] //I want the required data to be an array so that I can use .map()
If you want my actual code here it is
//ApiSlice
import { createSlice, createAsyncThunk } from "#reduxjs/toolkit";
export const getPosts = createAsyncThunk("posts/getPosts", async () => {
return fetch("https://api.jikan.moe/v4/anime?q=naruto&sfw").then((response) =>
response.json()
);
});
const postSlice = createSlice({
name: "posts",
initialState: {
posts: [],
loading: false,
},
extraReducers: {
[getPosts.pending]: (state, action) => {
state.loading = true;
},
[getPosts.fulfilled]: (state, action) => {
state.loading = false;
state.posts = action.payload;
},
[getPosts.rejected]: (state, action) => {
state.loading = false;
},
},
});
export default postSlice.reducer
//store
import { configureStore } from "#reduxjs/toolkit";
import postReducer from "./anime";
export const store =configureStore({
reducer: {
post:postReducer
}
})
//Api data
import React from "react";
import { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { getPosts } from "../store/anime";
function Apidata() {
const { posts, loading } = useSelector((state) => state.post);
const dispatch = useDispatch();
useEffect(() => {
dispatch(getPosts());
}, []);
console.log(posts.data)
return (
<div>
{posts.map((item) => (
<h2>{item.data}</h2>
))}
</div>
);
}
export default Apidata;
// App.js
import { Fragment, useState } from "react";
import Apidata from "./components/Apidata";
function App() {
return (
<Fragment>
<Apidata/>
</Fragment>
)
}
export default App;
if you want create an array just wrap the response.json() in an array like that:
export const getPosts = createAsyncThunk("posts/getPosts", async () => {
return fetch("https://api.jikan.moe/v4/anime?q=naruto&sfw")
.then(response=>response.json())
.then((response) =>[response]
);
});
BUT I don't think it is a best practice. Ask to whom create the backend and get explanations!.
Hope the best for you,
Mauro
This peace of code resolved my issue
const myObj = {0 : {mal_id: 20, url: 'https://myanimelist.net/anime/20/Naruto', images: 'test', trailer: 'test', approved: true}, 1: {mal_id: 20, url: 'https://myanimelist.net/anime/20/Naruto', images: 'test', trailer: 'test', approved: true}};
const myArr = [];
for (const key in myObj) {
const arrObj = myObj[key];
arrObj['id'] = key;
myArr.push(arrObj)
}
console.log(myArr);
Click here to see the reddit post:
Solution reddit link
Please help me understand why am I getting no items on first request.
I have this component ExportData that calls Context only when prompted, dialogConfirmation is linked to a button, snippet:
ExportData:
import useItem from '../../context/ItemsContext'
const ExportData = () => {
const { items, getAll } = useItem()
const dialogConfirmation = confirm => {
if (confirm) {
getAll()
}
}
}
ItemsContext:
import React, {
useState,
useReducer,
createContext,
useContext,
useEffect,
} from 'react'
import { useQuery } from '#apollo/client'
import { CURRENT_MONTH, ALL_ITEMS } from '../graphql/queries'
import itemReducer from '../reducers/itemReducer'
export const ItemProvider = ({ children }) => {
let items = []
const [all, setAll] = useState(false)
const selectedMonth = dayjs(new Date()).format('YYYY-MM')
const [state, dispatch] = useReducer(itemReducer, [])
const result = useQuery(all ? ALL_ITEMS : CURRENT_MONTH, {
variables: all ? null : { selectedMonth },
})
if (result.data && result.data.getCurrentMonth) {
items = [...result.data.getCurrentMonth]
}
if (result.data && result.data.getItems) {
items = [...result.data.getItems]
}
useEffect(() => {
if (result.data && result.data.getCurrentMonth) {
dispatch({
type: 'ALL',
items: result.data.getCurrentMonth,
})
}
if (result.data && result.data.getItems) {
dispatch({
type: 'ALL',
items: result.data.getItems,
})
}
}, [result.data])
const getAll = () => {
setAll(true)
}
const value = {
items: state.items,
getAll,
}
return <ItemContext.Provider value={value}>{children}</ItemContext.Provider>
}
Reducer:
const itemReducer = (state, action) => {
switch (action.type) {
case 'ALL':
return {
...state,
items: action.items,
}
default:
return state
}
}
Query CURRENT_MONTH returns only items from current month - that should be the default one.
Query ALL_ITEMS returns all items in database.
What happens currently, when I use the getAll function in ExportData, on the first try, it returns only the items from current month, however on the second try, it returns correct output(all data).
When debugging itemReducer, all items are in the action.items however the output within the component gets only the old data...On the second try however, it works fine.
What am I doing wrong here?
What is happening here:
Component is initially rendered (no data yet)
useEffects are fired: (inside component) does nothing, (inside useQuery) fires request to gql api
after some time when request is fulfilled state inside useQuery is set with data
Becouse of that component gets rerender (data is present but not yet used in state.items becouse dispatch was not fired yet)
useEffects are fired and this time dispatch is called.
Component rerenders again because dispatch changed useReducer state and this time state has items inside
In this process react renders 3 times. If you place console.log(state.items) inside component it will look like this:
[] // no data yet
[] // data is present but not yet used by `state`
[<your data>] // data is present and used by state
You can solve that by removing useEffect and useReducer:
Component is initially rendered (no data yet)
useEffects are fired: (inside component) does nothing, (inside useQuery) fires request to gql api
after some time when request is fulfilled state inside useQuery is set with data
Becouse of that component gets rerender (data is passed directly to value)
In this process react renders 2 times. If you place console.log(items) inside component it will look like this:
[] // no data yet
[<your data>] // data is present and used
import React, {
useState,
createContext,
useContext,
} from 'react'
import { useQuery } from '#apollo/client'
import { CURRENT_MONTH, ALL_ITEMS } from '../graphql/queries'
export const ItemProvider = ({ children }) => {
const [all, setAll] = useState(false)
const selectedMonth = dayjs(new Date()).format('YYYY-MM')
const result = useQuery(all ? ALL_ITEMS : CURRENT_MONTH, {
variables: all ? null : { selectedMonth },
})
const getAll = () => {
setAll(true)
}
const items = result?.data.getCurrentMonth || result?.data.getItems || []
const value = {
items,
getAll,
}
return <ItemContext.Provider value={value}>{children}</ItemContext.Provider>
}
Basically I got a state called optimizer in which I store a field named optimizer_course_entries , this field has 2 reducers on it:
import { createSlice } from "#reduxjs/toolkit";
const initialState = {
optimizer_course_entries : [],
}
export const optimizerSlice = createSlice(
{
name: 'optimizer',
initialState,
reducers: {
edit_entry: (state, action) => {
console.log('CALLED EDIT REDUCERS');
state.optimizer_course_entries[action.payload.index] = action.payload.value;
},
reset: (state) => {
console.log('CALLED RESET REDUCER');
state.optimizer_course_entries = [];
}
}
}
)
export const {edit_entry, reset} = optimizerSlice.actions;
export default optimizerSlice.reducer;
In my react app, I have a call to edit_entry everything a textbox is edited, and it sends the index and value in a payload to Redux.
const receiveChange = (Value, Index) => {
dispatch(edit_entry({
index : Index,
value : Value,
}));
}
I have the reset reducer set on component mount like this:
React.useEffect(
() => {
dispatch(reset());
} , []
)
The issue i'm having is that on component mount, instead of redux only doing a reset, it also restores previous reducer actions..
And in my redux store, the optimizer_course_entries entry is identical to before the reset...
I'm still pretty new to redux, is there a way I can specify it so that upon re-mount it doesn't do this repopulation?
I am a new React developer, implementing global state in my app. Im using useReducer with Context API to cache form search data, but I feel I'm using the reducer wrong, even if it works. I am preparing payload BEFORE calling dispatchSearchData, instead of doing it directly inside reducer:
import React, { createContext, useReducer, useMemo, useEffect } from "react";
const initialData = {
from: "",
to: "",
date_go: "",
date_back: "",
passengers: "",
};
const dataReducer = (searchData, newData) => {
if (newData === null) {
localStorage.removeItem("currentSearchData");
return initialData;
}
return { ...searchData, ...newData };
};
const localData = JSON.parse(localStorage.getItem("currentSearchData"));
export const SearchDataContext = createContext({});
export const SearchDataProvider = (props) => {
const [searchData, dispatchSearchData] = useReducer(dataReducer, localData || initialData);
const searchDataValue = useMemo(
() => ({
searchData,
setSearchData,
}),
[searchData, setSearchData],
);
useEffect(() => {
localStorage.setItem("currentSearchData", JSON.stringify(searchData));
}, [searchData]);
return <SearchDataContext.Provider value={searchDataValue}>{props.children}</SearchDataContext.Provider>;
};
An example of calling it:
let search = (e) => {
e.preventDefault();
dispatchSearchData(formData);
setServiceData(null);
}
I need to fetch my data in two different ways and render it according to this. At the first load, I need to fetch all the items one by one and increment the count. After that, I need to fetch all the data at once and update the display. So, I wrote something like this (not the actual code but almost the same thing):
import React, { useEffect } from "react";
import axios from "axios";
import { useGlobalState } from "./state";
const arr = Array.from(Array(100), (x, i) => i + 1);
function App() {
const [{ posts }, dispatch] = useGlobalState();
useEffect(() => {
const getInc = () => {
arr.forEach(async id => {
const res = await axios(
`https://jsonplaceholder.typicode.com/posts/${id}`
);
dispatch({
type: "INC",
payload: res.data
});
});
};
const getAll = async () => {
const promises = arr.map(id =>
axios(`https://jsonplaceholder.typicode.com/posts/${id}`)
);
const res = await Promise.all(promises);
dispatch({
type: "ALL",
payload: res.map(el => el.data)
});
};
if (!posts.length) {
getInc();
} else {
getAll();
}
}, [dispatch]);
return (
<>
<div>{posts.length}</div>
</>
);
}
export default App;
I'm simply using Context and useReducer to create a simple store. The above code works as it is but I skip adding posts.length dependency and this makes me think that my logic is wrong.
I tried to use refs to keep track the initialization state but I need to track the data at every route change. Then, I tried to keep it by adding an init state to my store but I couldn't make it work without problems. For example, I can't find a suitable place to dispatch the init. If I try it after a single fetch it triggers the initialization immediately and my other function (getAll) is invoked.
If anyone wants to play with it here is a working sandbox: https://codesandbox.io/s/great-monad-402lb
I added init to your store:
// #dataReducer.js
export const initialDataState = {
init: true,
posts: []
};
const dataReducer = (state, action) => {
switch (action.type) {
case 'ALL':
// init false
return { ...state, posts: action.payload };
case 'INC':
return { ...state, init: false, posts: [...state.posts, action.payload] };
...
}
// #App.js
function App() {
const [{ init, posts }, dispatch] = useGlobalState();
useEffect(() => {
init ? getInc(dispatch) : getAll(dispatch);
}, [init, dispatch]);
...
}