Cannot get the updated the state value useState and UseEffect - javascript

I want to get the latest value of the state and use it inline style, but it firstly returns 0 values, then rendering updated the state. However, I cannot assign the values into the style.
const ImageCard = ({ image: { alt_description, urls } }) => {
const [spans, setSpans] = useState(0);
const imageRef = useRef(null);
useEffect(() => imageRef.current.addEventListener('load', () => {
const height = imageRef.current.clientHeight;
const spans = Math.ceil(height / 10);
setSpans({ spans });
}));
return (
<div style={{ gridRowEnd: `span ${spans}` }}>
<img ref={imageRef} alt={alt_description} src={urls.regular} />
</div>
);
}
console output:
10 0
{spans: 15}
{spans: 33}
...

You would not need useEffect for this. You can use the onLoad event of the img tag as follows:
const ImageCard = ({ image: { alt_description, urls } }) => {
const [spans, setSpans] = useState(0);
const imageLoaded = e => {
const height = e.currentTarget.clientHeight;
const spans = Math.ceil(height / 10);
setSpans({ spans });
};
//Dont forget to remove this line
console.log(spans);
return (
<div style={{ gridRowEnd: `span ${spans}` }}>
<img onLoad={imageLoaded} alt={alt_description} src={urls.regular} />
</div>
);
};
Working code sandbox example

Related

Check if a button click sets array length to 2 with React testing library

I'm testing a React app for the first time and I'm struggling to write tests that check if an array in a component has 2 elements on first render and on clicking a button.
The error I'm getting is TypeError: Expected container to be an Element, a Document or a DocumentFragment but got string.
Here's the component where I need to test usersCards - it needs to have two elements on first render and every time the user clicks 'deal'.
I'm not sure how to deal with variables in components - do I mock it up in the test file? Ant help appreciated!
\\imports
export default function Home(){
const startHandSize = 2
const [starterDeck, setStarterDeck] = useState(shuffle(deckArray))
const [howManyDealt, setHowManyDealt] = useState(startHandSize)
const [total, setTotal] = useState(0)
const [ace, setAce] = useState(0)
const deal = () => {
setHowManyDealt(startHandSize)
setStarterDeck(shuffle(deckArray))
setAce(0)
}
const hit = () => !bust && setHowManyDealt(prev => prev + 1)
const usersCards = starterDeck.slice(-howManyDealt)
const bust = total > 21;
useEffect(() => {
setTotal(usersCards.reduce((a, e) => a + e.value, 0) + ace)
}, [ace, usersCards])
return(
<div>
{
<>
<button data-testid="deal" onClick={deal}>DEAL</button>
<button data-testid="hit" disabled={bust} onClick={hit}>HIT</button>
<button disabled={bust}>STAND</button>
<Total total={total}/>
{usersCards.map(card => (
<Card data-testid="test-card" key={card.index}
card={card} setTotal={setTotal} total={total}
ace={ace} setAce={setAce}
/>
))}
</>}
</div>
)
}
Here's the test:
//Deal button test
test("on initial render, two cards are displayed", () => {
render(<Home />)
const cards = getAllByTestId('test-card')
expect(cards.length).toEqual(2)
})
I guess something like that would work:
test("on initial render, two cards are displayed", () => {
const { getAllByTestId } = render(<Home />);
const cards = getAllByTestId('test-card');
expect(cards.length).toEqual(2);
});
test("two new cards should be displayed after clicking the button", () => {
const { getAllByTestId, getByTestId } = render(<Home />);
const dealButton = getByTestId('deal');
fireEvent.click(dealButton);
const cards = getAllByTestId('test-card');
expect(cards.length).toEqual(2);
});

How to display posts on the current page from the API

I'm getting data from Django Rest API and React for Frontend, and I need to create the pagination with posts. I did it all in pagination component. I created the state with current page and I'm changing it by clicking on the page button in component like this:
const Paginator = () => {
const [count, setCount] = useState(null);
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(null);
const [nextPage, setNextPage] = useState(null);
const [previousPage, setPreviousPage] = useState(null);
const [valid, setValid] = useState(false);
useEffect(() => {
fetch(`http://127.0.0.1:8000/api/software/?p=${currentPage}`)
.then(response => response.json())
.then(data => {
setCount(data.count);
setTotalPages(data.total_pages)
setNextPage(data.links.next);
setPreviousPage(data.links.previous);
setValid(true);
})
}, [currentPage]);
...
return (
<>
{
...
<PbStart style={style} totalPages={range(1, totalPages+1)} setCurrentPage={setCurrentPage} />
...
}
</>
);
};
const PagItem = ({key, handleClick, className, title, name }) => {
return (
<li key={key} onClick={handleClick}>
<Link to='/' className={className} title={`Go to page ${title}`}>
{name}
</Link>
</li>
);
};
const PbStart = ({ style, totalPages, setCurrentPage }) => {
return (
...
{totalPages.map(p => (
<PagItem key={p} handleClick={() => setCurrentPage(p)} title={p} name={p} />
))}
...
);
};
And in posts component I don't know how to change current page, or getting it from the paginaton component. I've written that like this:
const Softwares = () => {
const [softwares, setSoftwares] = useState([]);
const [currentPage, setCurrentPage] = useState(1);
const [valid, setValid] = useState(false);
useEffect(() => {
fetch(`http://127.0.0.1:8000/api/software/?p=${currentPage}`)
.then(response => response.json())
.then(data => {
setSoftwares(data.results);
setValid(true);
})
}, [currentPage]);
return (
<>
{
...
{softwares.map(s => (
<Article key={s.id} pathname={s.id} title={s.title} image={s.image} pubdate={s.pub_date} icon={s.category.parent.img} categoryID={s.category.id} categoryName={s.category.name} dCount={s.counter} content={s.content} />
))}
...
}
</>
);
};
So, how to do that(get the current page from pagination component or another way)?
I think a Paginator's job is only moving between pages and updating current page state. It should not be fetching data by itself, you can provide functionality to do extra work with props.
I haven't tested this, but this might be a good starting point.
With the example below you'll have a list of articles and then below it next and previous buttons.
In Softwares, as you can see I am passing the same function for handling next and previous pages, you can refactor it to have one function like onPageMove and call this function handleNext and handlePrev.
I added two separate functions if you have want to handle something different in either.
const Paginator = ({
total, // Required: Total records
startPage = 1, // Start from page / initialize current page to
limit = 30, // items per page
onMoveNext = null, // function to call next page,
onMovePrev = null, // function to call previous page
}) => {
const [currentPage, setCurrentPage] = useState(startPage);
const canGoNext = total >= limit;
const canGoPrev = currentPage > 1;
function handleNext(e) {
if (canGoNext) {
setCurrentPage((prevState) => prevState+1);
onMoveNext && onMoveNext({ currentPage });
}
}
function handlePrev(e) {
if (canGoPrev) {
setCurrentPage((prevState) => prevState-1);
onMovePrev && onMovePrev({ currentPage });
}
}
return (
<div>
<button onClick={handlePrev} disabled={!canGoPrev}>Prev</button>
<button onClick={handleNext} disabled={!canGoNext}>Next</button>
</div>
);
};
Here is how you can use Paginator in other components.
const PER_PAGE = 30; // get max # of records per page
const Softwares = () => {
const [softwares, setSoftwares] = useState([]);
const [valid, setValid] = useState(false);
const onFetchData = ({ currentPage }) => {
fetch(`http://127.0.0.1:8000/api/software/?p=${currentPage}&per_page=${PER_PAGE}`)
.then(response => response.json())
.then(data => {
setSoftwares(data.results);
setValid(true);
})
}
useEffect(() => {
onFetchData({ currentPage: 1 })
}, []);
return (
<>
{softwares.map(s => (
<Article key={s.id} pathname={s.id} title={s.title} image={s.image} pubdate={s.pub_date} icon={s.category.parent.img} categoryID={s.category.id} categoryName={s.category.name} dCount={s.counter} content={s.content} />
))}
<Paginator total={softwares.length} limit={PER_PAGE} onMoveNext={onFetchData} onMovePrev={onFetchData} />
</>
);
};

In React, how can I calculate the width of a parent based on it's child components?

I am working on a draggable carousel slider in React and I'm trying to figure out how to calculate the total width of the containing div based on the number of child elements it contains. I assumed I would be able to get the width after the component mounted but it didn't return the result I was expecting...I now assume it needs to wait for the images to be loaded and mounted to do this. What would be the best way to get the width of the <div className="carousel__stage">?
const Carousel = () => {
const carouselStage = useRef(null);
useEffect ( () => {
console.log(carouselStage.scrollWidth);
}, [carouselStage]);
return (
<div className="carousel">
<div ref={carouselStage} className="carousel__stage">
<Picture />
<Picture />
<Picture />
<Picture />
<Picture />
</div>
</div>
);
}
Sounds like a use case for ResizeObserver API.
function useResizeObserver() {
const [size, setSize] = useState({ width: 0, height: 0 });
const resizeObserver = useRef(null);
const onResize = useCallback((entries) => {
const { width, height } = entries[0].contentRect;
setSize({ width, height });
}, []);
const ref = useCallback(
(node) => {
if (node !== null) {
if (resizeObserver.current) {
resizeObserver.current.disconnect();
}
resizeObserver.current = new ResizeObserver(onResize);
resizeObserver.current.observe(node);
}
},
[onResize]
);
useEffect(
() => () => {
resizeObserver.current.disconnect();
},
[]
);
return { ref, width: size.width, height: size.height };
}
Pass a callback function to each Pictures and use useState hook to calculate total width.
const [width, setWidth] = useState(200); //default width
const handleImageLoaded = (size) =>{
setWidth(width + size)
}
Use ref to get image's width like you already did and pass it to the callback function .

React+gsap use random animation element group can't work when it complete?

I use gsap to create the animation.
When the button is clicked creates a bubble animation.
When animation is completed destroys itself.
I think the question is use map at React component but I can't find another case
Here is my React code and js fiddle example:
https://jsfiddle.net/xiaowang/ueqsg83j/58/
const { useState, useEffect, useRef } = React;
gsap.registerPlugin(MotionPathPlugin)
const Bubble = ({ onClose, data }) => {
const pointRef = useRef(null)
useEffect(() => {
const path = []
let offsetY = 0
for(let i = 0; i < 10; i++) {
const y = offsetY - Math.floor(Math.random() * 20 + 30)
offsetY = y
path.push({ x: Math.floor(Math.random() * 40 - 20), y })
}
gsap.to(pointRef.current, 5, {
motionPath: {
path,
type: 'cubic'
},
onComplete: () => onClose()
})
return () => {}
}, [])
return (<span className="bubble" ref={pointRef}>{data.id}</span>)
}
const App = () => {
const [count, setCount] = useState(0)
const [bubbles, setBubbles] = useState([])
const handleCreate = () => {
setBubbles([...bubbles, {id: count}])
setCount(count + 1)
}
const handleClose = index => {
const newBubbles = [...bubbles]
newBubbles.splice(index, 1)
setBubbles(newBubbles)
}
return (
<div className="wrap">
{
bubbles.map((item, index) => (
<Bubble
key={item.id}
data={item}
onClose={() => handleClose(index)} />
))
}
<button type="button" onClick={handleCreate}>Click Me</button>
</div>
)
}
ReactDOM.render(<App />, document.getElementById('app'))
Not sure it will help much, but I've moved your code to the sandbox, because I couldn't find the console on jsfiddle, and made small modification to the code:
https://codesandbox.io/s/react-and-gsap-issue-7tkrd
Main changes are.
Change handleClose implementation to a simple filter:
const handleClose = id => () => {
setBubbles(prev => prev.filter(bubble => bubble.id !== id));
};
Changed how we invoke it (and render in general):
return (
<div className="wrap">
{bubbles.map(item => (
<Bubble key={item.id} data={item} onClose={handleClose(item.id)} />
))}
<button type="button" onClick={handleCreate}>
Click Me
</button>
</div>
);
It has fixed the issue, if I understood your problem correctly, and I think that problem was in how you have used splice. Hope that will help.

How to trigger the useCallback in React

I am having a React(version used 16.8) component, I have a const updateDiffText = useCallback(() callback on click of anchor GENERATE DIFF onclick onClick={updateDiffText} I call call back this updateDiffText
My requirement is I don't want one anchor code in my UI, I want whenever I have oldText and newText It should trigger the method updateDiffText and show the result. User should not click on anchor link to perform this.
My Code sand box here - https://codesandbox.io/s/react-diff-view-demo-htp06
if I have values in oldtext and newText it should call updateDiffText this method
My Code -
const DiffViewer = props => {
const oldText = useInput(props.startupConfigData);
const newText = useInput(props.runningConfigData);
const [{ type, hunks }, setDiff] = useState("");
const updateDiffText = useCallback(() => {
const diffText = formatLines(diffLines(oldText.value, newText.value), {
context: 3
});
const [diff] = parseDiff(diffText, { nearbySequences: "zip" });
setDiff(diff);
}, [oldText.value, newText.value, setDiff]);
const tokens = useMemo(() => tokenize(hunks), [hunks]);
return (
<div style={{ height: "450px", overflow: "auto" }}>
<a href="#" onClick={updateDiffText}>
GENERATE DIFF
</a>
{setDiff ? (
<Diff
viewType="split"
diffType={type}
hunks={hunks || EMPTY_HUNKS}
tokens={tokens}
>
{hunks => hunks.map(hunk => <Hunk key={hunk.content} hunk={hunk} />)}
</Diff>
) : (
""
)}
</div>
);
};
Let me know if query is not clear. Thanks.
Try to use the useEffect instead of useCallback. In your case you are are not calling the memoized function in the render stage. useCallback will return a memoized function. Check the modified version.
https://codesandbox.io/s/react-diff-view-demo-izdyi
const updateDiffText = useCallback(() => {
const diffText = formatLines(diffLines(oldText.value, newText.value), {
context: 3
});
const [diff] = parseDiff(diffText, { nearbySequences: "zip" });
setDiff(diff);
}, [props.startupConfigData, props.runningConfigData]);
to
const updateDiffText = useCallback(() => {
const diffText = formatLines(diffLines(oldText.value, newText.value), {
context: 3
});
const [diff] = parseDiff(diffText, { nearbySequences: "zip" });
setDiff(diff);
}, [oldText.value, newText.value, setDiff]);
////////////// Older solution before i understood ///////////////////////
////////////// New solution i suggest ///////////////////////////////////
const updateDiffText = () => {
// do what you wanna do
}
and use useEffect instead of useCallback like this
useEffect(() => {
updateDiffText();
},[props.startupConfigData, props.runningConfigData])

Categories