I have this function component for rendering a span as a bar. When I render the component, I pass the prop scrolled as false. Then, jQuery updates the attribute to true when I have scrolled 150 pixels. I know it's not good practice to use jQuery, and I am migrating from it. For now, I want to get this to work as I am still learning functional components.
export default function Button(props) {
const [scrolled, setScrolled] = useState( props.scrolled );
useEffect(() => {
setScrolled(props.scrolled);
}, [scrolled]);
return (
<HamburgerButton scrolled={scrolled}>
<p>{scrolled}</p>
<span></span>
</HamburgerButton>
);
}
And this styled component definition:
const HamburgerButton = styled.div`
...
span {
background: ${props => props.scrolled === 'false' ? props.theme.white : props.theme.black};
}
...
`;
When I scroll, I see the attribute scrolled has changed in the DOM from 'false' to 'true' but the spans stay white. Also, the paragraph tag with {scrolled} doesn't change from false.
Issue
Your code memoizes the props.scrolled value in the hooks.
export default function Button(props) {
const [scrolled, setScrolled] = useState( props.scrolled ); // state initialized
useEffect(() => { // hook called first render
setScrolled(props.scrolled); // state updated with same value
}, [scrolled]); // state value never changes during life of component so effect hook never recomputes
return (
<HamburgerButton scrolled={scrolled}>
<p>{scrolled}</p>
<span></span>
</HamburgerButton>
);
}
Solution
You can directly pass prop to HamburgerButton
export default function Button(props) {
return (
<HamburgerButton scrolled={props.scrolled}>
<p>{props.scrolled}</p>
<span></span>
</HamburgerButton>
);
}
Or use the hooks and use the correct dependency
export default function Button(props) {
const [scrolled, setScrolled] = useState( props.scrolled );
useEffect(() => {
setScrolled(props.scrolled); // update state
}, [props.scrolled]); // with the value that changes here
return (
<HamburgerButton scrolled={scrolled}>
<p>{scrolled}</p>
<span></span>
</HamburgerButton>
);
}
Use a positive comparison for the true branch of a ternary and leverage javascript's truthy/falsey values. If scrolled is true, or any other truthy value, render black, if fasley, (false, 0, null, undefined) render white.
const HamburgerButton = styled.div`
...
span {
background: ${props => props.scrolled ? props.theme.black : props.theme.white};
}
...
`;
or
const HamburgerButton = styled.div`
...
span {
background: ${props => props.theme[props.scrolled ? 'black' : 'white']};
}
...
`;
Related
I'm playing around with a hook that can store some deleted values. No matter what I've tried, I can't get the state from this hook to update when I use it in a component.
const useDeleteRecords = () => {
const [deletedRecords, setDeletedRecords] = React.useState<
Record[]
>([]);
const [deletedRecordIds, setDeletedRecordIds] = React.useState<string[]>([]);
// ^ this second state is largely useless – I could just use `.filter()`
// but I was experimenting to see if I could get either to work.
React.useEffect(() => {
console.log('records changed', deletedRecords);
// this works correctly, the deletedRecords array has a new item
// in it each time the button is clicked
setDeletedRecordIds(deletedRecords.map((record) => record.id));
}, [deletedRecords]);
const deleteRecord = (record: Record) => {
console.log(`should delete record ${record.id}`);
// This works correctly - firing every time the button is clicked
setDeletedRecords(prev => [...prev, record]);
};
const wasDeleted = (record: Record) => {
// This never works – deletedRecordIds is always [] when I call this outside the hook
return deletedRecordIds.some((r) => r === record.id);
};
return {
deletedRecordIds,
deleteRecord,
wasDeleted,
} // as const <-- no change
}
Using it in a component:
const DisplayRecord = ({ record }: { record: Record }) => {
const { deletedRecordIds, wasDeleted, deleteRecord } = useDeleteRecords();
const handleDelete = () => {
// called by a button on a row
deleteRecord(record);
}
React.useEffect(() => {
console.log('should fire when deletedRecordIds changes', deletedRecordIds);
// Only fires once for each row on load? deletedRecordIds never changes
// I can rip out the Ids state and do it just with deletedRecords, and the same thing happens
}, [deletedRecordIds]);
}
If it helps, these are in the same file – I'm not sure if there's some magic to exporting a hook in a dedicated module? I also tried as const in the return of the hook but no change.
Here's an MCVE of what's going on: https://codesandbox.io/s/tender-glade-px631y?file=/src/App.tsx
Here's also the simpler version of the problem where I only have one state variable. The deletedRecords state never mutates when I use the hook in the parent component: https://codesandbox.io/s/magical-newton-wnhxrw?file=/src/App.tsx
problem
In your App (code sandbox) you call useDeleteRecords, then for each record you create a DisplayRecord component. So far so good.
function App() {
const { wasDeleted } = useDeleteRecords(); // ✅
console.log("wtf");
return (
<div className="App" style={{ width: "70vw" }}>
{records.map((record) => {
console.log("was deleted", wasDeleted(record));
return !wasDeleted(record) ? (
<div key={record.id}>
<DisplayRecord record={record} /> // ✅
</div>
) : null;
})}
</div>
);
}
Then for each DisplayRecord you call useDeleteRecords. This maintains a separate state array for each component ⚠️
const DisplayRecord = ({ record }: { record: Record }) => {
const { deletedRecords, deleteRecord } = useDeleteRecords(); // ⚠️
const handleDelete = () => {
// called by a button on a row
deleteRecord(record);
};
React.useEffect(() => {
console.log("should fire when deletedRecords changes", deletedRecords);
// Only fires once for each row on load? deletedRecords never changes
}, [deletedRecords]);
return (
<div>
<div>{record.id}</div>
<div onClick={handleDelete} style={{ cursor: "pointer" }}>
[Del]
</div>
</div>
);
};
solution
The solution is to maintain a single source of truth, keeping handleDelete and deletedRecords in the shared common ancestor, App. These can be passed down as props to the dependent components.
function App() {
const { deletedRecords, deleteRecord, wasDeleted } = useDeleteRecords(); // 👍🏽
const handleDelete = (record) => (event) { // 👍🏽 delete handler
deleteRecord(record);
};
return (
<div className="App" style={{ width: "70vw" }}>
{records.map((record) => {
console.log("was deleted", wasDeleted(record));
return !wasDeleted(record) ? (
<div key={record.id}>
<DisplayRecord
record={record}
deletedRecords={deletedRecords} // 👍🏽 pass prop
handleDelete={handleDelete} // 👍🏽 pass prop
/>
</div>
) : null;
})}
</div>
);
}
Now DisplayRecord can read state from its parent. It does not have local state and does not need to call useDeleteRecords on its own.
const DisplayRecord = ({ record, deletedRecords, handleDelete }) => {
React.useEffect(() => {
console.log("should fire when deletedRecords changes", deletedRecords);
}, [deletedRecords]); // ✅ passed from parent
return (
<div>
<div>{record.id}</div>
<div
onClick={handleDelete(record)} // ✅ passed from parent
style={{ cursor: "pointer" }}
children="[Del]"
/>
</div>
);
};
code demo
I would suggest a name like useList or useSet instead of useDeleteRecord. It's more generic, offers the same functionality, but is reusable in more places.
Here's a minimal, verifiable example. I named the delete function del because delete is a reserved word. Run the code below and click the ❌ to delete some items.
function App({ items = [] }) {
const [deleted, del, wasDeleted] = useSet([])
React.useEffect(_ => {
console.log("an item was deleted", deleted)
}, [deleted])
return <div>
{items.map((item, key) =>
<div className="item" key={key} data-deleted={wasDeleted(item)}>
{item} <button onClick={_ => del(item)} children="❌" />
</div>
)}
</div>
}
function useSet(iterable = []) {
const [state, setState] = React.useState(new Set(...iterable))
return [
Array.from(state), // members
newItem => setState(s => (new Set(s)).add(newItem)), // addMember
item => state.has(item) // isMember
]
}
ReactDOM.render(
<App items={["apple", "orange", "pear", "banana"]}/>,
document.querySelector("#app")
)
div.item { display: inline-block; border: 1px solid dodgerblue; padding: 0.25rem; margin: 0.25rem; }
[data-deleted="true"] { opacity: 0.3; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<div id="app"></div>
Since you are updating deletedRecordIds inside a React.useEffect, this variable will have the correct value only after the render complete. wasDeleted is a closure that capture the value of deletedRecordIds when the component renders, thus it always have a stale value. As yourself are suggesting, the correct way to do that is to use .filter() and remove the second state.
Talking about the example you provided in both cases you are defining 5 hooks: one hook for each DisplayRecord component and one for the App. Each hook define is own states, thus there are 5 deletedRecords arrays on the page. Clicking on Del, only the array inside that specific component will be updated. All other component won't be notified by the update, because the state change is internal to that specific row. The hook state in App will never change because no one is calling its own deleteRecord function.
You could solve that problem in 2 way:
Pulling up the state: The hook is called just once in the App component and the deleteRecord method is passed as parameter to every DisplayRecord component. I updated your CodeSandbox example.
Use a context: Context allows many component to share the same state.
I have a React Typescript functional component like below,
const Comp = (): JSX.Element => {
const [showNext, setShowNext] = useState(false);
const [data1, setData1] = useState(array);
return showNext ? <AnotherComponent /> : <></>;
};
If I move to the next component and coming back to the previous one, then state values are gone in the previous component. So, in UI already entered inputs are gone. How can I avoid this, or how can I store it?
By using conditional rendering you unmount the component depending on the state, unmounting component loses its state.
Therefore try hiding it with CSS or save its state in the parent component (Comp in this case)
// Save state in parent
const Comp = () => {
const [nextState, setNextState] = useState({})
return showNext ? <AnotherComponent {...nextState}/> : <></>;
};
// Or hide it with CSS
const Comp = () => {
const [nextState, setNextState] = useState({})
return <AnotherComponent className={showNext ? 'myClassName' : 'hideClass'} />;
};
// Possible CSS
.hideClass {
display: none;
}
.myClassName{
display: block;
}
I would like to change the background of a div if the window is scrolled to the top. I have figured out how to determine that in React, and when console.logging my isAtTop variable it will change based on the scroll, however the actual div never seems to receive as the styled.div doesn't change colors. I have made a codePen: https://codesandbox.io/s/xenodochial-chebyshev-4poi3?file=/src/App.js
import "./styles.css";
import React from 'react'
import styled from 'styled-components'
export default function App() {
let isAtTop = true;
let bg = `linear-gradient(to right, #797cd2, #393e9e)`;
function handleScroll() {
isAtTop = window.scrollY === 0;
isAtTop
? (bg = `linear-gradient(to right, #797cd2, #393e9e)`)
: (bg = "white");
console.log({ isAtTop });
}
React.useEffect(() => {
console.log("UseEffect Run");
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
});
const ColorChangeDiv = styled.div`
background: ${bg};
position: sticky;
width: 100%;
top: 0;
height: 100px;
`
return (
<>
<ColorChangeDiv>
{isAtTop.toString()} ? Hello my background should be {bg}
</ColorChangeDiv>
<div style={{height: '500rem'}}></div>
</>
);
}
I changed your code a little, now I will explain what exactly changed.
Made the functions arrow, so it looks cleaner
Wrapped the scroll event handler in useCallback to exclude unnecessary calls to useEffect, since now this function is in useEffect dependencies and it is a good practice to specify all dependencies used in useEffect.
The background color is now stored in the state. You were storing in a variable, the component is not re-rendered on changing the let variable. This means that even changing the value of the variable, the component will not display itself with the new values. We need a rerender. A change in state causes a rerender. This means that if you want to change the component depending on the changed values, then store them in a state to call the rerender :)
I brought the styled component out, it looks cleaner and plus, it makes no sense to keep it inside the APP component, you can pass the values to the styled component in props. Check out the example below, now the background is passed as props
You have long default background values. And it repeats itself. Such long repetitive things are better to be translated into constants with good understandable names. See I brought it up as DEFAULT_BG.
https://codesandbox.io/s/boring-pasteur-18v0r?file=/src/App.js:361-372
import React, { useCallback, useState, useEffect } from "react";
import styled from "styled-components";
const DEFAULT_BG = `linear-gradient(to right, #797cd2, #393e9e)`;
const ColorChangeDiv = styled.div`
background: ${(p) => p.bg};
position: sticky;
width: 100%;
top: 0;
height: 2000px;
`;
const App = () => {
const [bg, setBg] = useState(DEFAULT_BG);
const handleScroll = useCallback(() => {
setBg(window.scrollY === 0 ? DEFAULT_BG : "white");
}, []);
useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, [handleScroll]);
return (
<>
<ColorChangeDiv bg={bg}>
Hello my background should be {bg}
</ColorChangeDiv>
<div style={{ height: "500rem" }}></div>
</>
);
};
export default App;
Following simple component from the official examples:
import {useSpring, animated} from 'react-spring'
function App() {
const props = useSpring({opacity: 1, from: {opacity: 0}})
return <animated.div style={props}>I will fade in</animated.div>
}
Question
How do I animate the fadeIn-effect (or any other animation) again for example when I click on a button or when a promise is resolved?
You can basically make two effect with useSpring and an event.
You can change the style for example the opacity depending on the state of an event.
You can restart an animation on state change. The easiest way to restart is to rerender it.
I created an example. I think you want the second case. In my example I rerender the second component with changing its key property.
const Text1 = ({ on }) => {
const props = useSpring({ opacity: on ? 1 : 0, from: { opacity: 0 } });
return <animated.div style={props}>I will fade on and off</animated.div>;
};
const Text2 = () => {
const props = useSpring({ opacity: 1, from: { opacity: 0 } });
return <animated.div style={props}>I will restart animation</animated.div>;
};
function App() {
const [on, set] = React.useState(true);
return (
<div className="App">
<Text1 on={on} />
<Text2 key={on} />
<button onClick={() => set(!on)}>{on ? "On" : "Off"}</button>
</div>
);
}
Here is the working example: https://codesandbox.io/s/upbeat-kilby-ez7jy
I hope this is what you meant.
This is a bit of a strange one and not sure why it's happening exactly.
When the component mounts, I call a function that in my application makes an HTTP request to get an array of Objects. Then I update 3 states within a map method.
enquiries - Which is just the response from the HTTP request
activeProperty - Which defines which object id is current active
channelDetails - parses some of the response data to be used as a prop to pass down to a child component.
const [enquiries, setEnquiries] = useState({ loading: true });
const [activeProperty, setActiveProperty] = useState();
const [channelDetails, setChannelDetails] = useState([]);
const getChannels = async () => {
// In my actual project,this is an http request and I filter responses
const response = await Enquiries;
const channelDetailsCopy = [...channelDetails];
setEnquiries(
response.map((e, i) => {
const { property } = e;
if (property) {
const { id } = property;
let tempActiveProperty;
if (i === 0 && !activeProperty) {
tempActiveProperty = id;
setActiveProperty(tempActiveProperty);
}
}
channelDetailsCopy.push(getChannelDetails(e));
return e;
})
);
setChannelDetails(channelDetailsCopy);
};
useEffect(() => {
getChannels();
}, []);
Then I return a child component ChannelList that uses styled components to add styles to the element and renders child elements.
const ChannelList = ({ children, listHeight }) => {
const ChannelListDiv = styled.div`
height: ${listHeight};
overflow-y: scroll;
overflow-x: hidden;
`;
return <ChannelListDiv className={"ChannelList"}>{children}</ChannelListDiv>;
};
Inside ChannelList component I map over the enquiries state and render the ChannelListItem component which has an assigned key on the index of the object within the array, and accepts the channelDetails state and an onClick handler.
return (
<>
{enquiries &&
enquiries.length > 0 &&
!enquiries.loading &&
channelDetails.length > 0 ? (
<ChannelList listHeight={"380px"}>
{enquiries.map((enquiry, i) => {
return (
<ChannelListItem
key={i}
details={channelDetails[i]}
activeProperty={activeProperty}
setActiveProperty={id => setActiveProperty(id)}
/>
);
})}
</ChannelList>
) : (
"loading..."
)}
</>
);
In the ChannelListItem component I render two images from the details prop based on the channelDetails state
const ChannelListItem = ({ details, setActiveProperty, activeProperty }) => {
const handleClick = () => {
setActiveProperty(details.propId);
};
return (
<div onClick={() => handleClick()} className={`ChannelListItem`}>
<div className={"ChannelListItemAvatarHeads"}>
<div
className={
"ChannelListItemAvatarHeads-prop ChannelListItemAvatarHead"
}
style={{
backgroundSize: "cover",
backgroundImage: `url(${details.propertyImage})`
}}
/>
<div
className={
"ChannelListItemAvatarHeads-agent ChannelListItemAvatarHead"
}
style={{
backgroundSize: "cover",
backgroundImage: `url(${details.receiverLogo})`
}}
/>
</div>
{activeProperty === details.propId ? <div>active</div> : null}
</div>
);
};
Now, the issue comes whenever the chrome dev tools window is open and you click on the different ChannelListItems the images blink/rerender. I had thought that the diff algorithm would have kicked in here and not rerendered the images as they are the same images?
But it seems that styled-components adds a new class every time you click on a ChannelListItem, so it rerenders the image. But ONLY when the develop tools window is open?
Why is this? Is there a way around this?
I can use inline styles instead of styled-components and it works as expected, though I wanted to see if there was a way around this without removing styled-components
I have a CODESANDBOX to check for yourselves
If you re-activate cache in devtool on network tab the issue disappear.
So the question becomes why the browser refetch the image when cache is disabled ;)
It is simply because the dom change so browser re-render it as you mentioned it the class change.
So the class change because the componetn change.
You create a new component at every render.
A simple fix:
import React from "react";
import styled from "styled-components";
const ChannelListDiv = styled.div`
height: ${props => props.listHeight};
overflow-y: scroll;
overflow-x: hidden;
`;
const ChannelList = ({ children, listHeight }) => {
return <ChannelListDiv listHeight={listHeight} className={"ChannelList"}>{children}</ChannelListDiv>;
};
export default ChannelList;
I think it has to do with this setting to disable cache (see red marking in image)
Hope this helps.