Prevent useContext value from updating every instance of a mapped component - reactjs

I have a long list of components which takes a value from my Context.
Component has a unique id, and also knows which is the "current" id. I only want the Component whose id matches the currentId to update when the value from myContext changes.
const ComponentList = () => {
return(
<>
{data.map((d,i) =>
<Component key={i} id={d.id} />
}
</>
)
}
const Component = ({id}) => {
const {value} = useContext(MyContext);
const {currentId} = useContext(OtherContext);
//only re-render if `id === currentId` and `value` changes
return (
<h1>{value}</h1>
)
}
Note that the value from myContext does not care about which Component id is the current id.
I figure the answer lies somewhere in memoization but I haven't been successful in finding it. (I was able to get this working as I intend by doing const {value} = id === currentId ? useContext(MyContext) : {value: null} but this is obviously not okay)
I've abstracted this to a very generic example but if more context (haha) is necessary I can explain my use case further.

Seems like React.memo was the answer and it was fairly simple; just didn't see it at first.
I wrapped the Component in a memo and added my own condition.
const ParentComponent = (props) => {
const {value} = useContext(MyContext);
const {currentId} = useContext(OtherContext);
return (
<Component id={props.id} value={value} currentId={currentId} />
)
}
const Component = memo(
({value}) => {
return (
<h1>{value}</h1>
)
},
(prev, next) =>
prev.value === next.value || prev.id !== next.currentId
);
This essentially will not update if the previous and next value are equal, or if the id does not match the currentId.

Related

I am trying to give an active className to mapped data when I click. Currently the previous active class doesn't change when i click on another text

Parent component
Here is the mapped function
function SubHeader() {
const categories = category?.data?.data;
return (
{categories?.map((data) => (
<Smaller data={data} />
))} );
}
Child component
Here is where I am using the state to control the color of the text when it is clicked. Not sure I can figure what isn't right.
function Smaller({ data }) {
const [active, setActive] = useState(false);
const colorPicker = (dataId) => {
setActive(dataId ? !active : active);
};
return (
<Text
color={active ? 'brand.blue' : 'brand.dark'}
onClick={() => colorPicker(data?.id)}
>
{data?.name}
</Text>
);
}
The issue is when you click on another text, it doesn't change the active state of other / previous texts. So you're just toggling the active class of each component but it doesn't "untoggle" anywhere.
I found a way to solve this but I used a simple example because I didn't have your data.
Solution:
Each child component should have an ID.
Check if the child component's ID matches the activeElementID.
Parent Component
function SubHeader() {
const [activeElementID, setActiveElementID] = useState()
const categories = category?.data?.data;
return (
{categories?.map((data) => {
<Smaller data = {data} id = {data?.id} activeElementID={activeElementID} setActiveElementID={setActiveElementID} />
})}
)
}
Child Component
function Smaller({data, id, activeElementID, setActiveElementID}) {
function clicked() {
setActiveElementID(id)
}
return (
<p onClick={clicked} className={activeElementID === id ? "blue" : "red"}>{data}</p>
)
}
Furthermore, I would also recommend checking the data instead of using the "?" operation. For example, category?.data and data?.data
Because you are saying that you are sure the data exists. You can do this instead.
const categories = category.data.data
if (categories) {
return (....)
}
Hope this helps!

Conditionally assign ref in react

I'm working on something in react and have encountered a challenge I'm not being able to solve myself. I've searched here and others places and I found topics with similar titles but didn't have anything to do with the problem I'm having, so here we go:
So I have an array which will be mapped into React, components, normally like so:
export default ParentComponent = () => {
//bunch of stuff here and there is an array called arr
return (<>
{arr.map((item, id) => {<ChildComponent props={item} key={id}>})}
</>)
}
but the thing is, there's a state in the parent element which stores the id of one of the ChildComponents that is currently selected (I'm doing this by setting up a context and setting this state inside the ChildComponent), and then the problem is that I have to reference a node inside of the ChildComponent which is currently selected. I can forward a ref no problem, but I also want to assign the ref only on the currently selected ChildComponent, I would like to do this:
export default ParentComponent = () => {
//bunch of stuff here and there is an array called arr and there's a state which holds the id of a selected ChildComponent called selectedObjectId
const selectedRef = createRef();
return (<>
<someContextProvider>
{arr.map((item, id) => {
<ChildComponent
props={item}
key={id}
ref={selectedObjectId == id ? selectedRef : null}
>
})}
<someContextProvider />
</>)
}
But I have tried and we can't do that. So how can dynamically assign the ref to only one particular element of an array if a certain condition is true?
You can use the props spread operator {...props} to pass a conditional ref by building the props object first. E.g.
export default ParentComponent = () => {
const selectedRef = useRef(null);
return (
<SomeContextProvider>
{arr.map((item, id) => {
const itemProps = selectedObjectId == id ? { ref: selectedRef } : {};
return (
<ChildComponent
props={item}
key={id}
{...itemProps}
/>
);
})}
<SomeContextProvider />
)
}
You cannot dynamically assign ref, but you can store all of them, and access by id
export default ParentComponent = () => {
//bunch of stuff here and there is an array called arr and theres a state wich holds the id of a selected ChildComponent called selectedObjectId
let refs = {}
// example of accessing current selected ref
const handleClick = () => {
if (refs[selectedObjectId])
refs[selectedObjectId].current.click() // call some method
}
return (<>
<someContextProvider>
{arr.map((item, id) => {
<ChildComponent
props={item}
key={id}
ref={refs[id]}
>
})}
<someContextProvider />
</>)
}
Solution
Like Drew commented in Medets answer, the only solution is to create an array of refs and access the desired one by simply matching the index of the ChildElement with the index of the ref array, as we can see here. There's no way we found to actually move a ref between objects, but performance cost for doing this should not be relevant.

What's the difference between useObserver and observer in this application?

I have a react functional component that accesses the MobX store with useContext. I have found two ways to observe an array that is an observable from the store. First, the useObserver hook and wrapping the component with observer.
I thought that these are the same but that the useObserver only observes specific properties (such as the array that is passed) but I am experiencing a problem when the array reaches size 2 and then the component does not re-render. That's the case when using useObserver. When wrapping with observer, this is fixed.
Can anyone explain why this is happening and what's the difference?
const ApplesContainer = observer(() => {
const stores = useStores();
const applesArray = stores.fruits.apples;
return (
{applesArray.map(apple => (
<Apple key={apple.id} apple={apple} />
))}
);
});
// OR with useObserver()
function useGlobalState() {
const stores = useStores();
return useObserver(() => ({
applesArray: stores.fruits.apples
}));
}
const ApplesContainer = observer(() => {
const { applesArray } = useGlobalState();
return (
{applesArray.map(apple => (
<Apple key={apple.id} apple={apple} />
))}
);
});
useObserver must return JSX with an observable value.
This hook takes care of tracking changes and re-rendering them.
If no observable value exists in JSX, then it won't be re-rendered.
e.g.:
const SomeContainer =() => {
const { someStores } = useStores();
return useObserver(()=>(
{someStore.data.map(val => (
<Apple key={val.id} val={val} />
))}
));
};

React useState, value of input doesnt update

I wish to generate inputs based on json, so first I set it to initial state, then in child componenet I want to modify it's field, thing is that component doesnt update... It renders once and have no idea how to make it be updated each time when input onChange change it's value. Any idea how to make value of input be updated each time when I type something?
PARENT
function App() {
const [inputValue, setInputValue] = useState(chunkingRow);
const handleChunkingChange = (e, index) => {
let inputContent = inputValue;
const reg = new RegExp(/^[0-9]+$/);
if (e.target.value.match(reg)) {
inputContent[index] = {
...inputContent[index],
content: e.target.value,
};
setInputValue(inputContent);
console.log(inputValue);
} else console.log('not a number')
};
return (
<div>
<Wrapper>
{Chunk(inputValue, handleChunkingChange)}
</Wrapper>
</div>
);
}
CHILD
const Chunk = (inputValue, handleChunkingChange) => {
return(
<div>
{inputValue.map((el, index) => (
<div key={index}>
<p>{el.title}</p>
{console.log(el.content)}
<input
type="text"
onChange={(e, i) => handleChunkingChange(e, index)}
value={el.content}
/>
</div>
))}
</div>
);
}
link to demo
https://codesandbox.io/s/stoic-mirzakhani-46exz?file=/src/App.js
Not completely sure why this happens, but probably because of the way you handle the input change. It seems to me that component doesn't recognize that array changed. How I managed to fix your code is replacing line 9 in App component with following code:
let inputContent = [...inputValue];
By doing that, array's reference is changed and components are updated.
Just update your code as follow:
let inputContent = [ ...inputValue ];
You are mutating the state object.
let inputContent = inputValue;
That's why the state is not re-rendered. Change it to
let inputContent = [...inputValue];
An example of mutating objects. React compares previous state and current state and renders only if they are different.
const source = { a: 1, b: 2 };
const target = source;
console.log(target);
console.log(target === source); = true
target.b = 99;
console.log({target});
console.log({source}); //source == target due to mutation
console.log(source === target); = true
Remember, never mutate.

How to target a specific item to toggleClick on using React Hooks?

I have a navbar component with that actual info being pulled in from a CMS. Some of the nav links have a dropdown component onclick, while others do not. I'm having a hard time figuring out how to target a specific menus index with React Hooks - currently onClick, it opens ALL the dropdown menus at once instead of the specific one I clicked on.
The prop toggleOpen is being passed down to a styled component based on the handleDropDownClick event handler.
Heres my component.
const NavBar = props => {
const [links, setLinks] = useState(null);
const [notFound, setNotFound] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const fetchLinks = () => {
if (props.prismicCtx) {
// We are using the function to get a document by its uid
const data = props.prismicCtx.api.query([
Prismic.Predicates.at('document.tags', [`${config.source}`]),
Prismic.Predicates.at('document.type', 'navbar'),
]);
data.then(res => {
const navlinks = res.results[0].data.nav;
setLinks(navlinks);
});
}
return null;
};
const checkForLinks = () => {
if (props.prismicCtx) {
fetchLinks(props);
} else {
setNotFound(true);
}
};
useEffect(() => {
checkForLinks();
});
const handleDropdownClick = e => {
e.preventDefault();
setIsOpen(!isOpen);
};
if (links) {
const linkname = links.map(item => {
// Check to see if NavItem contains Dropdown Children
return item.items.length > 1 ? (
<Fragment>
<StyledNavBar.NavLink onClick={handleDropdownClick} href={item.primary.link.url}>
{item.primary.label[0].text}
</StyledNavBar.NavLink>
<Dropdown toggleOpen={isOpen}>
{item.items.map(subitem => {
return (
<StyledNavBar.NavLink href={subitem.sub_nav_link.url}>
<span>{subitem.sub_nav_link_label[0].text}</span>
</StyledNavBar.NavLink>
);
})}
</Dropdown>
</Fragment>
) : (
<StyledNavBar.NavLink href={item.primary.link.url}>
{item.primary.label[0].text}
</StyledNavBar.NavLink>
);
});
// Render
return (
<StyledNavBar>
<StyledNavBar.NavContainer wide>
<StyledNavBar.NavWrapper row center>
<Logo />
{linkname}
</StyledNavBar.NavWrapper>
</StyledNavBar.NavContainer>
</StyledNavBar>
);
}
if (notFound) {
return <NotFound />;
}
return <h2>Loading Nav</h2>;
};
export default NavBar;
Your problem is that your state only handles a boolean (is open or not), but you actually need multiple booleans (one "is open or not" for each menu item). You could try something like this:
const [isOpen, setIsOpen] = useState({});
const handleDropdownClick = e => {
e.preventDefault();
const currentID = e.currentTarget.id;
const newIsOpenState = isOpen[id] = !isOpen[id];
setIsOpen(newIsOpenState);
};
And finally in your HTML:
const linkname = links.map((item, index) => {
// Check to see if NavItem contains Dropdown Children
return item.items.length > 1 ? (
<Fragment>
<StyledNavBar.NavLink id={index} onClick={handleDropdownClick} href={item.primary.link.url}>
{item.primary.label[0].text}
</StyledNavBar.NavLink>
<Dropdown toggleOpen={isOpen[index]}>
// ... rest of your component
Note the new index variable in the .map function, which is used to identify which menu item you are clicking.
UPDATE:
One point that I was missing was the initialization, as mention in the other answer by #MattYao. Inside your load data, do this:
data.then(res => {
const navlinks = res.results[0].data.nav;
setLinks(navlinks);
setIsOpen(navlinks.map((link, index) => {index: false}));
});
Not related to your question, but you may want to consider skipping effects and including a key to your .map
I can see the first two useState hooks are working as expected. The problem is your 3rd useState() hook.
The issue is pretty obvious that you are referring the same state variable isOpen by a list of elements so they all have the same state. To fix the problems, I suggest the following way:
Instead of having one value of isOpen, you will need to initialise the state with an array or Map so you can refer each individual one:
const initialOpenState = [] // or using ES6 Map - new Map([]);
In your fetchLink function callback, initialise your isOpen state array values to be false. So you can put it here:
data.then(res => {
const navlinks = res.results[0].data.nav;
setLinks(navlinks);
// init your isOpen state here
navlinks.forEach(link => isOpen.push({ linkId: link.id, value: false })) //I suppose you can get an id or similar identifers
});
In your handleClick function, you have to target the link object and set it to true, instead of setting everything to true. You might need to use .find() to locate the link you are clicking:
handleClick = e => {
const currentOpenState = state;
const clickedLink = e.target.value // use your own identifier
currentOpenState[clickedLink].value = !currentOpenState[clickedLink].value;
setIsOpen(currentOpenState);
}
Update your component so the correct isOpen state is used:
<Dropdown toggleOpen={isOpen[item].value}> // replace this value
{item.items.map(subitem => {
return (
<StyledNavBar.NavLink href={subitem.sub_nav_link.url}>
<span>{subitem.sub_nav_link_label[0].text}</span>
</StyledNavBar.NavLink>
);
})}
</Dropdown>
The above code may not work for you if you just copy & paste. But it should give you an idea how things should work together.

Resources