How to update a component according to the state? - reactjs

I need to display a list of objects in my Explorer component.
In my app I use useReducer hook what wrapped by Context.
It works well when the data flow is in "input-mode" (when I update data in state). But it does not rerender the application after data was changed.
So, steps that I need to pass and get a positive result.
Press btn with file icon (or folder icon). This btn call hook that take a look into state and make a decision: where this file or folder should be placed in my simple fs.
Write file/folder name (this function doesn't exist yet).
Apply name by press enter or click mouse out of input (the same as 2 step).
Currently, I try to create file/folder with hardcoded name for testing 1-step. And I expect that dispatch function pass data to the state and it would be updated and rerendered. All process runs well except of rerender.
I explain the flow of 1-st step.
After I click btn, I call the func from hook for forming my instance.
Then, the new instance saving into local useState.
After local useState successfully was updated, I call dispatch in useEffect hook.
In reducer I modify my state and return it.
After this steps I expect that my app will automatically rerendered, but it isn't.
Code snippets, step by step.
First step.
const handleFileClick = () => {
formAnInstance('file');
console.log('file btn click')
};
Second step.
// in useInstancesInteraction hook
const { state, dispatch } = useStateContext();
const [instance, setInstance] = useState<IInstance>();
const formAnInstance = (mode: Omit<Mode, 'root'>) => {
if (
typeof state?.currentFolder === 'undefined' ||
state?.currentFolder === null
) {
const target =
mode === 'folder'
? (createInstance('folder', 'folder') as IInstance)
: (createInstance('file', 'file') as IInstance);
target['..'] = '/';
setInstance(target);
}
};
Third step.
// in useInstancesInteraction hook
useEffect(() => {
const updateData = () => {
if (dispatch && instance) {
dispatch(createRootInstance(instance));
}
};
updateData();
}, [instance]);
Fourth step.
export const initialState = {
root: createInstance('/', 'root') as IInstance,
currentFolder: null,
};
const reducer = (state = initialState, action: IAction) => {
const { type, payload } = action;
switch (type) {
case ACTION_TYPES.CREATE_ROOT_INSTANCE:
const myKey = payload['.'];
Object.assign(state.root, { [myKey]: payload });
console.log('Reducer', state?.root);
return state;
case ACTION_TYPES.CREATE_FILE:
break;
case ACTION_TYPES.UPLOAD_FILE:
break;
case ACTION_TYPES.RENAME_FILE:
break;
case ACTION_TYPES.DELETE_FILE:
break;
case ACTION_TYPES.CREATE_FOLDER:
break;
case ACTION_TYPES.RENAME_FOLDER:
break;
case ACTION_TYPES.DELETE_FOLDER:
break;
default:
return state;
}
};
Here how my context file look like:
import React, { useContext, useReducer } from 'react';
import { IContext } from './index.types';
import reducer, { initialState } from './reducer';
const StateContext = React.createContext<IContext>({
state: undefined,
dispatch: null,
});
const StateProvider = ({
children,
}: {
children: JSX.Element | JSX.Element[];
}) => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<StateContext.Provider value={{ state, dispatch }}>
{children}
</StateContext.Provider>
);
};
export default StateProvider;
export const useStateContext = () => useContext(StateContext);

Related

Redux set state only if state is different

I am using Redux for state management, but I encountered a problem. My issue is I like to set state only if state is different. Let me clarify my problem through my code.
// MyComponent.jsx
const [query, setQuery] = useState('');
useEffect(() => {
if(query.length) {
let {search, cancel} = searchContent(query);
search.then(res =>
setSearchResult(res.data)
).catch(e => {
if(axios.isCancel(e)){
return;
}
})
return () => cancel();
}else{
setSearchResult(null);
}
}, [query, setSearchResult])
Above is my component that is supposed to set search state.
// action.js
export const SET_SEARCH_RESULT = 'SET_SEARCH_RESULT';
export const setSearchResult = (val) => ({
type: SET_SEARCH_RESULT,
searchResult: val,
});
//reducer.js
import { SET_SEARCH_RESULT } from './article.action';
const INITIAL_STATE = {
searchResult: null
}
const articleReducer = (state=INITIAL_STATE, action) => {
switch (action.type) {
case SET_SEARCH_RESULT:
return {
...state,
searchResult: action.searchResult
}
default:
return state
}
}
I am able to set state using redux and it works fine. However, my problem is even though initial state is null, when useEffect function runs initially my state sets to null.
My question is how can I use redux so that only it runs if state is different.
Thanks in advance.

Why is not my immer reducer returning the new value even though the draft have changed?

Why is not my immer reducer returning the new value even though the draft have changed inside the reducer? I am using console.log to check if my draft is changed inside the immerReducer:
// inside my component that produces the immer
function MyComponent() {
const immerReducer = produce(reducer);
const [state, dispatch] = useReducer(immerReducer, initialState);
const contextValue = useMemo(() => [state, dispatch], [state, dispatch]);
const location = useLocation();
useEffect(() => console.log("mounted"), []); // mounted twice
useEffect(() => {
dispatch({ type: ACTIONS.SET_MODE, pathname: location.pathname })
}, [location.pathname]);
console.log(state.mode); // I can se that this returns the "old" value, the initial value
return (
<MyContext.Provider value={contextValue}>
{state.mode === MODES.DELETE ? <Deleted /> : <New />
</MyContext.Provider>
);
}
// inside the immer reducer file
export const reducer = (draft, action) => {
switch (action.type) {
case ACTIONS.SET_MODE:
if (action.pathname.endsWith(MODES.DELETE)) {
draft.mode = MODES.DELETE;
} else if (action.pathname.endsWith(MODES.EDIT)) {
draft.mode = MODES.EDIT;
} else {
draft.mode = MODES.NEW;
}
console.log("draft", draft.mode); // I can see that this logs out the wanted value
return draft;
...
}
}
, and it seems like it is changed, but when the state should have been updated, the value that is returned is the old value. Can this have anything to do with a new mounting that causes the initial state? It seemes like it is mounting twice. Can that have anything to do with me having a functional component for the component holdingn the immer reducer?
All help is appreciated!
Possibly produce inside Component is re-producing the reducer on each render. Try changing these to
// inside my component that produces the immer
function MyComponent() {
const immerReducer = produce(reducer); //<-- Remove it from here it is producing new reducer
// inside the immer reducer file
export const reducer = (draft, action) => { //<-- Put produce here
// export const reducer = produce(draft, action) => {} //<- Like this

Data leaking between different calls of the same custom hook (using useReducer)

I'm experiencing some odd behaviour with a custom hook that I've written. I use it as a staging ground to update information locally, before the user commits that information and sends it off to the database. This hook is called in 2 different places: when the user is creating an item (a nugget), and when a user is editing an item.
Naturally, we want each scenario to have its own state, and to not have data leak between the two. However, this is exactly what happens. In the EditScreen, I call the hook, passing it the data that already exists (nuggetInput). In the NewScreen, I also call the hook, passing in nothing. Strangely, when I go to the EditScreen, the hook implementation in NewScreen gets updated. Of course, I'd expect these 2 hooks to be isolated from one another.
To test if this was in fact the case, I separated this useStaging hook out into 2 identical hooks: useNewStaging and useEditStaging. Sure enough, this works and there are no leaks.
Here is the useStaging hook in question (with parts removed for brevity):
useStaging.js
let initialState = {
title: null,
mediaItems: [],
}
const reducer = (state, { type, ...action }) => {
switch (type) {
case 'TITLE_UPDATE':
return produce(state, (draft) => {
const { data } = action
draft.title = data
})
default:
throw new Error('Called reducer without supported type.')
}
}
export default (nuggetInput) => {
if (nuggetInput) {
initialState = merge(initialState, nuggetInput)
}
const [state, dispatch] = useReducer(reducer, initialState)
const updateTitle = (data) => dispatch({ type: 'TITLE_UPDATE', data })
return {
nuggetStaging: state,
updateTitle,
}
}
NewScreen.js
const NewScreen = () => {
const { nuggetStaging, updateTitle } = useStaging()
}
EditScreen.js
const EditScreen = ({ inputNugget }) => {
const { nuggetStaging, updateTitle } = useStaging(inputNugget)
}
Any idea why this would be happening?

How to update React UI from with state in Redux store with useEffect

I have the following functional component. I did debugging and read some posts about react-redux and useEffect, but still have had no success. On initial render the state in my redux store is null but then changes to reflect new state with data. However, my react UI does not reflect this. I understand what the issue is, but I don't know exactly how to fix it. I could be doing things the wrong way as far as getting the data from my updated state in the redux store.
Here is my component :
const Games = (props) => {
const [gamesData, setGamesData] = useState(null)
const [gameData, setGameData] = useState(null)
const [gameDate, setGameDate] = useState(new Date(2020, 2, 10))
const classes = GamesStyles()
// infinite render if placed in
// useEffect array
const {gamesProp} = props
useEffect(() => {
function requestGames() {
var date = parseDate(gameDate)
try {
props.getGames(`${date}`)
// prints null, even though state has changed
console.log(props.gamesProp)
setGamesData(props.gamesProp)
} catch (error) {
console.log(error)
}
}
requestGames()
}, [gameDate])
// data has not been loaded yet
if (gamesData == null) {
return (
<div>
<Spinner />
</div>
)
} else {
console.log(gamesData)
return (
<div><p>Data has been loaded<p><div>
{/* this is where i would change gameDate */}
)
}
}
const mapStateToProps = (state) => {
return {
gamesProp: state.gamesReducer.games,
}
}
const mapDispatchToProps = (dispatch) => {
return {
getGames: (url) => dispatch(actions.getGames(url)),
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Games)
Here is my reducer
import {GET_GAMES} from '../actions/types'
const initialState = {
games: null // what we're fetching from backend
}
export default function(state = initialState, action){
switch(action.type){
case GET_GAMES:
// prints correct data from state
//console.log(action.payload)
return{
...state,
games: action.payload
}
default:
return state
}
}
Here is my action
import axios from 'axios'
import {GET_GAMES} from './types'
// all our request go here
// GET GAMES
export const getGames = (date) => dispatch => {
//console.log('Date', date)
axios.get(`http://127.0.0.1:8000/games/${date}`)
.then(res => {
dispatch({
type: GET_GAMES,
payload: res.data
})
}).catch(err => console.log(err))
}
When I place the props from state in my dependencies array for useEffect, the state updates but results in an infinite render because the props are changing.
This happens even if I destruct props.
Here is an image of my redux state after it is updated on the initial render.
You were running into issues because you were trying to set the state based off of data that was in a closure. The props.gamesProp within the useEffect you had would never update even when the parent data changed.
The reason why props.gamesProp was null in the effect is because in each render, your component essentially has a new instance of props, so when the useEffect runs, the version of props that the inner part of the useEffect sees is whatever existed at that render.
Any function inside a component, including event handlers and effects, “sees” the props and state from the render it was created in.
https://reactjs.org/docs/hooks-faq.html#why-am-i-seeing-stale-props-or-state-inside-my-function
Unless you have to modify gamesState within your component, I highly recommend that you don't duplicate the prop to the state.
I'd also recommend using useDispatch and useSelector instead of connect for function components.
Here's some modifications to your component based on what I see in it currently and what I've just described:
import { useDispatch, useSelector } from 'react-redux';
const Games = (props) => {
const gamesData = useSelector((state) => state.gamesReducer.games);
const dispatch = useDispatch();
const [gameData, setGameData] = useState(null);
const [gameDate, setGameDate] = useState(new Date(2020, 2, 10));
const classes = GamesStyles();
// infinite render if placed in
// useEffect array
useEffect(() => {
const date = parseDate(gameDate);
try {
dispatch(actions.getGames(`${date}`));
} catch (error) {
console.log(error);
}
}, [gameDate, dispatch]);
// data has not been loaded yet
if (gamesData == null) {
return (
<div>
<Spinner />
</div>
);
} else {
console.log(gamesData);
return (
<div>
<p>Data has been loaded</p>
</div>
// this is where i would change gameDate
);
}
};
export default Games;
If you need to derive your state from your props, here's what the React Documentation on hooks has to say:
https://reactjs.org/docs/hooks-faq.html#how-do-i-implement-getderivedstatefromprops

React context state value not updated in Consumer

The first value set to "search term" through the "dispatcher" persists after any subsequent calls and I'm trying to figure out why that is or where the error is.
I've got a <ContextProvider /> where a state for "search term" is defined, and the value for the "search term" might change by an event that is triggered by the <ContextConsumer />, or nested <ContextConsumer /> component by a "dispatcher". I'm finding that the desired state is not found, after the call to the "reducer", even considering that the "state" change is not immediately.
For brevity, the Components or the code posted below was simplified to isolate the subject, so there might be a few typos like not declared variables (as I've removed chunks of code that is not related).
The Context Provider looks like:
import React from 'react'
export const POSTS_SEARCH_RESULTS = 'POSTS_SEARCH_RESULTS'
export const GlobalStateContext = React.createContext()
export const GlobalDispatchContext = React.createContext()
const initialState = {
posts: [],
searchTerm: ''
}
const reducer = (state, action) => {
switch (action.type) {
case POSTS_SEARCH_RESULTS: {
return {
...state,
posts: action.posts,
searchTerm: action.searchTerm
}
}
default:
throw new Error('Bad Action Type')
}
}
const GlobalContextProvider = ({ children }) => {
const [state, dispatch] = React.useReducer(reducer, initialState)
return (
<GlobalStateContext.Provider value={state}>
<GlobalDispatchContext.Provider value={dispatch}>
{children}
</GlobalDispatchContext.Provider>
</GlobalStateContext.Provider>
)
}
export default GlobalContextProvider
The Consumer looks like:
const Search = () => {
const state = useContext(GlobalStateContext)
const { searchTerm, posts } = state
useEffect(() => {
console.log('[debug] <Search />: searchTerm: ', searchTerm);
}, [searchTerm])
return (
<>
<LoadMoreScroll searchTerm={searchTerm} posts={posts} postCursor={postCursor} />
</>
)
}
export default Search
Following up is the nested Consumer Children Component. The useEffect has a dependency for searchTerm; This value is set through the "dispatcher" and get through the useContenxt in a Consumer.
dispatch({ type: POSTS_SEARCH_RESULTS, posts: postsCached, searchTerm: term })
And consumed like so:
const state = useContext(GlobalStateContext)
const { searchTerm, posts } = state
And passed to, for example <LoadMoreScroll searchTerm={searchTerm} />
So, what I have and it fails is:
const LoadMoreScroll = ({ searchTerm, posts, postCursor }) => {
const dispatch = useContext(GlobalDispatchContext)
const [postsCached, setPostsCached] = useState(posts)
const [loading, setLoading] = useState(false)
const refScroll = useRef(null)
const [first] = useState(POSTS_SEARCH_INITIAL_NUMBER)
const [after, setAfter] = useState(postCursor)
const [isVisible, setIsVisible] = useState(false)
const [term, setTerm] = useState(searchTerm)
useEffect(() => {
loadMore({ first, after, term })
}, [isVisible])
useEffect(() => {
dispatch({ type: POSTS_SEARCH_RESULTS, posts: postsCached, searchTerm })
}, [postsCached])
useEffect(() => {
setTerm(searchTerm)
const handler = _debounce(handleScroll, 1200)
window.addEventListener('scroll', handler)
return () => window.removeEventListener('scroll', handler)
}, [searchTerm])
const handleScroll = () => {
const offset = -(window.innerHeight * 0.1)
const top = refScroll.current.getBoundingClientRect().top
const isVisible = (top + offset) >= 0 && (top - offset) <= window.innerHeight
isVisible && setIsVisible(true)
}
const loadMore = async ({ first, after, term }) => {
if (loading) return
setLoading(true)
const result = await searchFor({
first,
after,
term
})
const nextPosts = result.data
setPostsCached([...postsCached, ...nextPosts])
setAfter(postCursor)
setLoading(false)
setIsVisible(false)
}
return (
<div ref={refScroll} className={style.loaderContainer}>
{ loading && <Loader /> }
</div>
)
}
export default LoadMoreScroll
The expected result is to have <LoadMoreScroll />'s to pass to the "loadMore" function the latest value of "searchTerm" assigned by the "dispatcher", which fails. What it does instead is that it consumes the "initial value" from a first call to the "dispatcher". This is after the initial call to the "dispatcher" any subsequent "dispatcher" call:
dispatch({ type: POSTS_SEARCH_RESULTS, posts: postsCached, searchTerm: term })
That should update the Context "searchTerm", fails to do. In the source code above, the loadmore holds the initial value that was set!
Separate example the has a similar logic, works without any issues ( https://codesandbox.io/s/trusting-booth-1w40e?fontsize=14&hidenavigation=1&theme=dark )
Hope to update the issue above with a solution soon, in case somebody spots the issue, please let me know!
The codesandbox link works, but doesn't seem to be using the same pattern as the code above when it comes to creating and using context.
In the provided code you have created two separate providers. One has a value of state and one has a value of dispatch.
<GlobalStateContext.Provider value={state}>
<GlobalDispatchContext.Provider value={dispatch}>
The codesandbox however is using both state and dispatch within the same provider.
<Application.Provider value={{ state, dispatch }}>
Also it seems that GlobalContextProvider is exported, but I'm not sure if it is used to wrap any consumers.
Since there is a separation of dispatch and state, I am going to use this for my proposed solution.
The implementation seems correct, but in my opinion you could take this a step further and create two custom hooks, that expose only one way to provide the context value and only one way to consume it.
import React from "react";
export const POSTS_SEARCH_RESULTS = "POSTS_SEARCH_RESULTS";
//
// notice that we don't need to export these anymore as we are going to be
//
// using them in our custom hooks useGlobalState and useGlobalDispatch
//
//
const GlobalStateContext = React.createContext();
const GlobalDispatchContext = React.createContext();
const initialState = {
posts: [],
searchTerm: "",
};
const reducer = (state, action) => {
switch (action.type) {
case POSTS_SEARCH_RESULTS: {
return {
...state,
posts: action.posts,
searchTerm: action.searchTerm
};
}
default:
throw new Error("Bad Action Type");
}
};
const GlobalContextProvider = ({ children }) => {
const [state, dispatch] = React.useReducer(reducer, initialState);
return (
<GlobalStateContext.Provider value={state}>
<GlobalDispatchContext.Provider value={dispatch}>
{children}
</GlobalDispatchContext.Provider>
</GlobalStateContext.Provider>
);
};
// If any of these hooks is not being called within a function component
// that is rendered within the `GlobalContextProvider`,
// we throw an error
const useGlobalState = () => {
const context = React.useContext(GlobalStateContext);
if (context === undefined) {
throw new Error(
"useGlobalState must be used within a GlobalContextProvider"
);
}
return context;
};
const useGlobalDispatch = () => {
const context = React.useContext(GlobalDispatchContext);
if (context === undefined) {
throw new Error(
"useGlobalDispatch must be used within a GlobalContextProvider"
);
}
return context;
};
// We only export the custom hooks for state and dispatch
// and of course our`GlobalContextProvider`, which we are
// going to wrap any part of our app that
// needs to make use of this state
export { GlobalContextProvider, useGlobalState, useGlobalDispatch };
All I've added here is a couple of custom hooks that expose each of the contexts, i.e GlobalStateContext and GlobalDispatchContext and export them along with the GlobalContextProvider.
If we wanted to make this globally available throughout the app, we could wrap the GlobalContextProvider around the App component.
function App() {
return (
<div className="App">
<Search />
</div>
);
}
// If you forget to wrap the consumer with your provider, the custom hook will
// throw an error letting you know that the hook is not being called
// within a function component that is rendered within the
// GlobalContextProvider as it's supposed to
const AppContainer = () => (
<GlobalContextProvider>
<App />
</GlobalContextProvider>
);
export default AppContainer;
If you want to either use the state in any part of your app, or dispatch any action, you will need to import the relevant custom hook created earlier.
In your Search component this would look like the example below:
import { useGlobalState, useGlobalDispatch } from "./Store";
const Search = () => {
// Since we are doing this in our custom hook that is not needed anymore
// const state = useContext(GlobalStateContext)
// if you need to dispatch any actions you can
// import the useGlobalDispatch hook and use it like so:
// const dispatch = useGlobalDispatch();
const state = useGlobalState();
const { searchTerm, posts } = state
useEffect(() => {
console.log('[debug] <Search />: searchTerm: ', searchTerm);
}, [searchTerm])
return (
<>
<LoadMoreScroll searchTerm={searchTerm} posts={posts} postCursor={postCursor} />
</>
)
}
export default Search
Since there were a few parts missing in the codesandbox provided in the question, I've refactored it to a simplified working version of this concept here that hopefully will help solve your issue.
I've also found this article quite helpful when I had problems with Context API and hooks.
It is following that same pattern, I've been using this in production and have been quite happy with the results.
Hope that helps :)

Resources