How to save a component state after re-rendering? React js - javascript

There are some movie cards that clients can click on them and their color changes to gray with a blur effect, meaning that the movie is selected.
At the same time, the movie id is transferred to an array list. In the search bar, you can search for your favorite movie but the thing is after you type something in the input area the movie cards that were gray loses their style (I suppose because they are deleted and rendered again based on my code) but the array part works well and they are still in the array list.
How can I preserve their style?
Search Page:
export default function Index(data) {
const info = data.data.body.result;
const [selectedList, setSelectedList] = useState([]);
const [searchTerm, setSearchTerm] = useState('');
return (
<>
<main className={parentstyle.main_container}>
<NavBar />
<div className={style.searchbar_container}>
<CustomSearch
onChange={(e) => {
setSearchTerm(e.target.value);
}}
/>
</div>
<div className={style.card_container}>
{info
.filter((value) => {
if (searchTerm === '') {
return value;
} else if (
value.name
.toLocaleLowerCase()
.includes(searchTerm.toLocaleLowerCase())
) {
return value;
}
})
.map((value, key) => {
return (
<MovieCard
movieName={value.name}
key={key}
movieId={value._id}
selected={selectedList}
setSelected={setSelectedList}
isSelected={false}
/>
);
})}
</div>
<div>
<h3 className={style.test}>{selectedList}</h3>
</div>
</main>
Movie Cards Component:
export default function Index({ selected, movieName, movieId, setSelected }) {
const [isActive, setActive] = useState(false);
const toggleClass = () => {
setActive(!isActive);
};
useEffect(()=>{
})
const pushToSelected = (e) => {
if (selected.includes(e.target.id)) {
selected.splice(selected.indexOf(e.target.id), 1);
console.log(selected);
} else {
selected.push(e.target.id);
console.log(selected);
console.log(e.target);
}
setSelected([...selected]);
toggleClass();
};
return (
<div>
<img
className={isActive ? style.movie_selected : style.movie}
id={movieId}
name={movieName}
src={`images/movies/${movieName}.jpg`}
alt={movieName}
onClick={pushToSelected}
/>
<h3 className={style.title}>{movieName}</h3>
</div>
);
}

I can't directly test your code so I will assume that this is the issue:
Don't directly transform a state (splice/push) - always create a clone or something.
Make the setActive based on the list and not dependent. (this is the real issue why the style gets removed)
try this:
const pushToSelected = (e) => {
if (selected.includes(e.target.id)) {
// filter out the id
setSelected(selected.filter(s => s !== e.target.id));
return;
}
// add the id
setSelected([...selected, e.target.id]);
};
// you may use useMemo here. up to you.
const isActive = selected.includes(movieId);
return (
<div>
<img
className={isActive ? style.movie_selected : style.movie}
id={movieId}
name={movieName}
src={`images/movies/${movieName}.jpg`}
alt={movieName}
onClick={pushToSelected}
/>
<h3 className={style.title}>{movieName}</h3>
</div>
);

This is a very broad topic. The best thing you can do is look up "React state management".
As with everything in the react ecosystem it can be handled by various different libraries.
But as of the latest versions of React, you can first start by checking out the built-in tools:
Check out the state lifecycle: https://reactjs.org/docs/state-and-lifecycle.html
(I see in your example you are using useState hooks, but I am adding these for more structured explanation for whoever needs it)
Then you might want to look at state-related hooks such as useState: https://reactjs.org/docs/hooks-state.html
useEffect (to go with useState):
https://reactjs.org/docs/hooks-effect.html
And useContext:
https://reactjs.org/docs/hooks-reference.html#usecontext
And for things outside of the built-in toolset, there are many popular state management libraries that also work with React with the most popular being: Redux, React-query, Mobx, Recoil, Flux, Hook-state. Please keep in mind that what you should use is dependant on your use case and needs. These can also help you out to persist your state not only between re-renders but also between refreshes of your app. More and more libraries pop up every day.
This is an ok article with a bit more info:
https://dev.to/workshub/state-management-battle-in-react-2021-hooks-redux-and-recoil-2am0#:~:text=State%20management%20is%20simply%20a,you%20can%20read%20and%20write.&text=When%20a%20user%20performs%20an,occur%20in%20the%20component's%20state.

Related

How do I render a react portal modal with different children?

Right now, I have a react portal rendering a 'pinnable' side drawer modal that will display based on a redux state. The contents of the modal will have information based on where that modal was pinned from, in this case my notifications.
The problem I'm running into at the moment is that since the modal will be pinnable in multiple places, I'm not exactly sure of the logic on how to handle the modal contents if the modal is already pinned.
I've tried/considered the following:
Just have one portal render its children dynamically. Unfortunately the location of where the portal will be rendered does not contain the contents and logic of the modal, so I believe this can't be done.
Compare props.children and if they're not identical, render the newer portal and deconstruct the other. I'm hesitant to use this approach since I believe there's a better solution out there.
Render the portals based on Ids and deconstruct/reconstruct where needed if one exists. I'm leaning towards this approach, but again I'd like to see if there's a better one.
Portal location:
export default function PaperContainer() {
return <div id="pinned-container"></div>;
}
Portal:
export default function PinnedContainer(props) {
const pinned = useSelector(state => state.ui.isDrawerPinned);
return (
pinned &&
createPortal(
<div>
<div>{props.children}</div>
</div>
,
document.getElementById('pinned-container')
)
);
}
Where the portals are called (simplified for brevity):
export default function PortalCallLocationOne() {
const dispatch = useDispatch();
const pinContainer = () => {
dispatch(toggleDrawer());
};
return (
<>
<Button startIcon={<Icon>push_pin</Icon>} onClick={() => pinContainer}>
Pin Notification
</Button>
<PinnedContainer>
//Notification
</PinnedContainer>
</>
);
}
export default function PortalCallLocationTwo() {
const dispatch = useDispatch();
const pinContainer = () => {
dispatch(toggleDrawer());
};
return (
<>
<Button startIcon={<Icon>push_pin</Icon>} onClick={() => pinContainer}>
Pin List
</Button>
<PinnedContainer>
// List
</PinnedContainer>
);
</>
}
I tried going off of #3 and destroying pinned-container's first child if it existed and replace it with the new children. This didn't work since React was expecting that child and kept throwing failed to execute removeChild on node errors.
Unfortunately it looks like react is unable to replace portal children instead of appending them.
However, I was able to solve my issue by unpinning the portal and repinning it with redux actions.
export default function PinnedContainer(props) {
const pinned = useSelector(state => state.ui.isDrawerPinned);
useEffect(() => {
if (pinned) {
dispatch(clearPinned());
dispatch(pinDrawer(true));
}
}, []);
return (
pinned &&
createPortal(
<div>
<div>{props.children}</div>
</div>
,
document.getElementById('pinned-container')
)
);
}
Reducer:
export const initialState = {
isDrawerPinned: false,
}
export const reducer = (state = initialState, action) => {
switch(action.type){
case actionTypes.PIN_DRAWER:
return {
...state,
isDrawerPinned: action.isPinned ? action.isPinned : !state.isDrawerPinned,
};
case actionTypes.CLEAR_PINNED:
return {
...state,
isDrawerPinned: state.isDrawerPinned ? !state.isDrawerPinned : state.isDrawerPinned
};
}
}

Is good to use React.useMemo or React.useCallback inside component props?

I was thinking about how to code TailwindCSS cleaner in React. Since Tailwind is utility-first, it makes us inevitably end up with components (ex: className="w-full bg-red-500"). So, I tried to create a utility like this:
utils/tailwind.ts
const tw = (...classes: string[]) => classes.join(' ')
and call it inside:
components/Example.tsx
import { useState } from 'react'
import tw from '../utils/tailwind'
const Example = () => {
const [text, setText] = useState('')
return (
<div>
<input onChange={(e: any) => setText(e.target.value)} />
<div
className={tw(
'w-full',
'h-full',
'bg-red-500'
)}
>
hello
</div>
</div>
)
}
But, it will cause tw() to be re-called as always as text state is updated.
So, I decided to wrap tw() function using useMemo to prevent re-call since the tw() always returns the same value. But the code is like this:
import { useState, useMemo } from 'react'
import tw from '../utils/tailwind'
const Example = () => {
const [text, setText] = useState('')
return (
<div>
<input onChange={(e: any) => setText(e.target.value)} />
<div
className={useMemo(() => tw(
'w-full',
'h-full',
'bg-red-500'
), [])}
>
hello
</div>
</div>
)
}
Is it correct or good practice if I put useMemo like that? Thank you 🙏 .
Is it correct or good practice if I put useMemo like that?
Short answer - yes.
Long answer - it depends. It depends on how heavy the operation is. In your particular case, joining a couple of strings may not be such heavy calculation to make the useMemo worth to be used - it's good to remember that useMemo memoizes stuff and it takes memory.
Consider example below. In the first case, without useMemo, the tw function will be called with every App re-render, to calculate new className. However, if useMemo is used (with empty dependency array), tw will not be called and new className will not be calculated even if the App re-renders, due to the basic memoization. It will be called only once, on component mount.
Conclusion - it's a good practice to use useMemo, but rather for heavy operations, like mapping or reducing huge arrays.
export default function App() {
const [_, s] = useState(0);
return (
<div className="App">
<div className={tw(false, 'w-full', 'h-full', 'bg-red-500')}>div1</div>
<div
className={useMemo(
() => tw(true, 'w-full', 'h-full', 'bg-red-500'),
[],
)}
>
div2
</div>
<button onClick={() => s(Math.random())}>re-render</button>
</div>
);
}
Playground: https://codesandbox.io/s/distracted-liskov-tfm72c?file=/src/App.tsx
The issue here is that React will re-render the component every time it's state changes. (each time you setText).
If you want to prevent that from happening, then see if you really need this re-render hence what do you really need the input text for?
you do not HAVE to use state here to use the input value.
you could call another function on change which will not update the state, and use the input value there for whatever you need.
for example:
const Example = () => {
const onInputChange = (e) => {
const text = e.target.value
// do something with text
}
return (
<div>
<input onChange={(e: any) => onInputChange(e)} />
<div
className={useMemo(() => tw(
'w-full',
'h-full',
'bg-red-500'
), [])}
>
hello
</div>
</div>
)
}

Rendering Child Component from store

I have a component which has child components, i want to render these child components with different Ids. They are getting their data from store.The problem is they are rendered but with the same item. how can this be solved?
MultiImages Component
const MultiImages: () => JSX.Element = () => {
const values = ['500', '406', '614'];
return (
<div>
{values.map((val, index) => {
return <OneImage key={index} projectID={val} />;
})}
</div>
);
};
export default MultiImages;
OneImage Component
const OneImage: () => JSX.Element = ({ projectID }) => {
const projectData = useProjectDataStore();
const { getProject } = useAction();
useEffect(() => {
getProject(projectID ?? '');
}, []);
return (
<>
<div>
<img
src={projectData.picture}
}
/>
<div>
<a>
{projectData.projectName}
</a>
</div>
</div>
</>
);
};
export default OneImage;
Your issue here - you are calling in a loop, one by one fetch your projects, and each call, as far as we can understand from your example and comments override each other.
Your are doing it implicitly, cause your fetching functionality is inside your Item Component OneImage
In general, the way you are using global state and trying to isolate one from another nodes is nice, you need to think about your selector hook.
I suggest you, to prevent rewriting too many parts of the code, to change a bit your selector "useProjectDataStore" and make it depended on "projectID".
Each load of next project with getProject might store into your global state result, but instead of overriding ALL the state object, you might want to use Map(Dictionary) as a data structure, and write a result there and use projectID as a key.
So, in your code the only place what might be change is OneImage component
const OneImage: () => JSX.Element = ({ projectID }) => {
// making your hook depended on **projectID**
const projectData = useProjectDataStore(projectID);
const { getProject } = useAction();
useEffect(() => {
// No need of usage **projectID** cause it will inherit if from useProjectDataStore
getProject();
}, []);
return (
<>
<div>
<img
src={projectData.picture}
}
/>
<div>
<a>
{projectData.projectName}
</a>
</div>
</div>
</>
);
};
export default OneImage;
And inside of your useProjectDataStore store result into a specific key using projectID.
Your component OneImage will return what's in the return statement, in your case:
<>
<div>
<img
src={projectData.picture}
/>
<div>
<a>
{projectData.projectName}
</a>
</div>
</div>
</>
This tag <></> around your element is a React.fragment and has no key. This is the reason you get this error.
Since you already have a div tag wrapping your element you can do this:
<div key={parseInt(projectID)}>
<img
src={projectData.picture}
/>
<div>
<a>
{projectData.projectName}
</a>
</div>
</div>
You can also change the key to Math.floor(Math.random() * 9999).
Note that passing the prop key={index} is unnecessary, and is not advised to use index as keys in a react list.

How to avoid re-renders at the App level when I make a change to a child component (like a searchbar)?

I'm trying to make a searchbar React component that doesn't trigger an App-wide re-render when I type, yet allows me to use the query in other components/to make an API call.
Background:
I learned that stateless input components are good for reusability and creating controlled components. So state stays at parent (or App) level and the component's value gets passed in via props.
On the other hand, tracking the query's state at the App level causes ALL components to re-render (when the input's handleChange calls setQuery) and feels like a needless use of resources.
What am I missing here? Do I leave the query piece of state at the SearchBar level instead? Should I use React.memo or useCallback?
SearchBar component:
import React from 'react';
const Searchbar = ({ query, handleQueryChange }) => {
return (
<div className="field">
<label>Enter search term</label>
<input type="text" onChange={handleQueryChange} value={query}></input>
</div>
);
};
export default Searchbar;
And the App component
const App = () => {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const handleQueryChange = (e) => {
setQuery(e.currentTarget.value);
};
useEffect(() => {
function search() {
...makeAPIcallwith(query).then((result) => {setResults(result)})
};
if (query) {
const timer = setTimeout(() => {
search()}, 1000);
return () => clearTimeout(timer);
}
}, [query]);
return (
<div className="content-container">
<SearchBar query={query} handleQueryChange={handleQueryChange} />
<...Other React component not needing to re-render... />
</div>
);
};
export default App;
The tiniest optimization that you could make here is this:
const handleQueryChange = useCallback((e) => {
setQuery(e.currentTarget.value);
},[]);
It's not worth making. What you've shown is good idomatic react code.
I guess the other thing that you could do, if you haven't already because you haven't shown the code, is to help React out by encapsulating the results in a component like this:
return (
<div className="content-container">
<SearchBar query={query} handleQueryChange={handleQueryChange} />
<ListOfThings results={results}/>
</div>
);
Super tiny components, so tiny they seem almost trivially simple, is the name of the game in React. If your components are over 30-40 lines long, then 👎

How to re-render React components without actually changing state

In my React application I have a component called Value, which has several instances on multiple levels of the DOM tree. Its value can be shown or hidden, and by clicking on it, it shows up or gets hidden (like flipping a card).
I would like to make 2 buttons, "Show all" and "Hide all", which would make all these instances of the Value component to show up or get hidden. I created these buttons in a component (called Cases) which is a parent of each of the instances of the Value component. It has a state called mode, and clicking the buttons sets it to "showAll" or "hideAll". I use React Context to provide this chosen mode to the Value component.
My problem: after I click the "Hide All" button and then make some Value instances visible by clicking on them, I'm not able to hide all of them again. I guess it is because the Value components won't re-render, because even though the setMode("hideAll") function is called, it doesn't actually change the value of the state.
Is there a way I can make the Value instances re-render after calling the setMode function, even though no actual change was made?
I'm relatively new to React and web-development, I'm not sure if it is the right approach, so I'd also be happy to get some advices about what a better solution would be.
Here are the code for my components:
const ModeContext = React.createContext()
export default function Cases() {
const [mode, setMode] = useState("hideAll")
return (
<>
<div>
<button onClick={() => setMode("showAll")}>Show all answers</button>
<button onClick={() => setMode("hideAll")}>Hide all answers</button>
</div>
<ModeContext.Provider value={mode}>
<div>
{cases.map( item => <Case key={item.name} {...item}/> ) }
</div>
</ModeContext.Provider>
</>
)
}
export default function Value(props) {
const mode = useContext(ModeContext)
const [hidden, setHidden] = useState(mode === "showAll" ? false : true)
useEffect(() => {
if (mode === "showAll") setHidden(false)
else if (mode === "hideAll") setHidden(true)
}, [mode])
return (
hidden
? <span className="hiddenValue" onClick={() => setHidden(!hidden)}></span>
: <span className="value" onClick={() => setHidden(!hidden)}>{props.children}</span>
)
}
You first need to create your context before you can use it as a provider or user.
So make sure to add this to the top of the file.
const ModeContext = React.createContext('hideAll')
As it stands, since ModeContext isn't created, mode in your Value component should be undefined and never change.
If your components are on separate files, make sure to also export ModeContext and import it in the other component.
Example
Here's one way to organize everything and keep it simple.
// cases.js
const ModeContext = React.createContext('hideAll')
export default function Cases() {
const [mode, setMode] = useState("hideAll")
return (
<>
<div>
<button onClick={() => setMode("showAll")}>Show all answers</button>
<button onClick={() => setMode("hideAll")}>Hide all answers</button>
</div>
<ModeContext.Provider value={mode}>
<div>
{cases.map( item => <Case key={item.name} {...item}/> ) }
</div>
</ModeContext.Provider>
</>
)
}
export function useModeContext() {
return useContext(ModeContext)
}
// value.js
import { useModeContext } from './cases.js'
export default function Value(props) {
const mode = useContext(ModeContext)
const [hidden, setHidden] = useState(mode === "showAll" ? false : true)
useEffect(() => {
if (mode === "showAll") setHidden(false)
else if (mode === "hideAll") setHidden(true)
}, [mode])
return (
hidden
? <span className="hiddenValue" onClick={() => setHidden(!hidden)}></span>
: <span className="value" onClick={() => setHidden(!hidden)}>{props.children}</span>
)
}
P.S. I've made this mistake many times, too.
You shouldn't use a new state in the Value component. Your components should have an [only single of truth][1], in your case is mode. In your context, you should provide also a function to hide the components, you can call setHidden
Change the Value component like the following:
export default function Value(props) {
const { mode, setHidden } = useContext(ModeContext)
if(mode === "showAll") {
return <span className="hiddenValue" onClick={() => setHidden("hideAll")}></span>
} else if(mode === "hideAll") {
return <span className="value" onClick={() => setHidden("showAll")}>{props.children}</span>
} else {
return null;
}
)
}
P.S. Because mode seems a boolean value, you can switch between true and false.
[1]: https://reactjs.org/docs/lifting-state-up.html
There are a few ways to handle this scenario.
Move the state in the parent component. Track all visible states in the parent component like this:
const [visible, setVisibilty] = useState(cases.map(() => true))
...
<button onClick={() => setVisibilty(casses.map(() => false)}>Hide all answers</button>
...
{cases.map((item, index) => <Case key={item.name} visible={visible[index]} {...item}/> ) }
Reset the mode after it reset all states:
const [mode, setMode] = useState("hideAll")
useEffect(() => {
setMode("")
}, [mode])

Categories

Resources