Defer state update / rerender in React - javascript

I created a React component that sends a GraphQL query to the backend to retrieve an image as a base64-encoded string and display it when it's loaded. Until then, it displays a little loading spinner. The code looks more or less like this and works as expected:
const { data, loading, error } = useQuery(...)
const showImage = !loading && !error && data?.image?.base64
const showError = !loading && (error || !showImage)
return (
<div>
{loading && <img src={`/loading.gif`} />}
{showError && <img src={`/error.png`} />}
{showImage && <img src={`data:image/jpg;base64, ${data.image.base64}`}/>}
</div>
)
But on top of that, the control also allows some basic image manipulation (e.g. a button that tints the image purple), which I implemented with useState. Essentially:
const [ purple, setPurple ] = useState(false)
const { data, loading, error } = useQuery(/* pass `purple` to backend */)
const showImage = !loading && !error && data?.image?.base64
const showError = !loading && (error || !showImage)
return (
<div>
{loading && <img src={`/loading.gif`} />}
{showError && <img src={`/error.png`} />}
{showImage && <img src={`data:image/jpg;base64, ${data.image.base64}`}/>}
<input type={`checkbox`} onChange={_ => setPurple(!purple)} />
</div>
)
All of that works fine, too, except as soon as the checkbox is clicked, the old image disappears and I get the loading.gif until the new image is fetched. That's not unexpected but undesired. I'd much rather keep the old image around and set the new image once it arrives.
I experimented with writing the old image's base64 string to a useState hook and read it from there until it's replaced by the new image. That works, but I got the impression that the performance was not great. (There are many, many of these components on the site and when the user scrolls long enough, hundreds of them may be loaded, which leads to a bloated React state. Can that be a problem or did I imagine it? Can I profile this?)
Which brings me to my question: Is there a way in React to somehow defer the full rerender and keep the old state around for a while until the new state goes into effect?

Well, what you are looking, assuming you are using Apollo Client, is previousData hook result property
https://www.apollographql.com/docs/react/data/queries/#previousdata
As per documentation
An object containing the result from the most recent previous
execution of this query.
This value is undefined if this is the query's first execution.
So something like (by using image variable together with ?. (optional chaining) you can avoid the boolean variable to decide if to show the image)
const { data, loading, error, previousData } = useQuery(/* ... */);
const image = data?.image?.base64 || previousData?.image?.base64;
Note: I suggest you to change the loading logic to display the loading spinner only if it's loading and no image is set, to avoid both loading spinner and previous image displaying together. You can eventually hide the image if case of errors.

Related

Display an array of image Reactjs Firebase SDK 9

I am quite new to Reactjs and Firebase. Now im trying to display an array of image that fetching from a selected document ID in Firestore Web SDK9.
For example, I have an array of image like the following figure below:
I want to display all the image with url link in "imgUrls" field. At the first time, I let user choose their options with an unique ID and link it to the new page that contains all the information with that ID. Here is my code:
<Link to={`/view/${currentItem.id}`}>
{currentItem.data.dateCreate}
</Link>
Then, I fetched all the data related to this ID:
useEffect(() => {
const fetchDocById = async () => {
const docSnap = await getDoc(docRef)
if (docSnap.exists()) {
setUser({
...docSnap.data()
})
;
} else {
setUser({});
}
}
fetchDocById()}, [id]);
Then, I display all the information related to this ID, for example:
<p>Creator: {user.creator}</p>
<p>Creation Date: {user.dateCreate}</p>
These line of codes are working very well. However , I have an issue with displaying the image's dataset. I have try to use: <img src={user.imgUrls}></img>. This code worked with the array of only one item on this. If the array is more than one, it displayed nothing. I had took the picture of this error:
Besides this, I have tried to mapping the array by using:
<div>
{ user.imgUrls.map((url) => (<img src={url}> </img>)) }
</div>
But it said that Cannot read properties of undefined (reading 'map').
Could you please try to help me in this issues. I really do appreciate that.
Thank you so much!
useEffect is executed after the initial render of the component. You may have initialized user state with {}. So, when the component is rendered for the first time, user.imgUrls is undefined, cause there is no property named imgUrls in user object. And undefined doesn't have any function called map. That's why the error was shown.
You can solve it in many ways. But what's common in each approach is that don't call map function until user has a property called imgUrls.
Solution 1:using logical operator
<div>
{Array.isArray(user.imgUrls) && user.imgUrls.map((url) => (<img src={url}> </img>)) }
</div>
Solution 2:using ternary operator
<div>
{Array.isArray(user.imgUrls) ? user.imgUrls.map((url) => (<img src={url}> </img>)):null }
</div>
Solution 3:using optional chaining
<div>
{user.imgUrls?.map((url) => (<img src={url}> </img>)) }
</div>

React ref null after re-rendering element

I have a to-do list where tasks can be either a single big textarea (called dataArea here) or a list of those textareas. Those textareas should grow in height as content is added, which I do by setting the height to its scrollHeight on input (via handleInput). What I want to do is let folks toggle between that plain textarea and list of textareas (via toggleChecklist), using state to store the content.
However, when the content is set via state—not direct user input—the handleInput function isn't reached and I must set that from a different function or manually fire onInput. Either way, I believe I must use a ref (blobRef) to access that element to set its height. However, blobRef is null after toggling to/from checklist. Why is that?
Here's where that's [not] happening in full context (I think only the Form.js file is what needs looking at): https://github.com/werdnanoslen/tasks/blob/help/src/components/Form.js#L85
And here's some code previews:
const blobRef = useRef(null)
...
function handleInput(e, i) {
const element = e.target
if (checklist) {
let checklistDataCopy = [...checklistData]
checklistDataCopy[i] = { ...checklistDataCopy[i], data: element.value }
setChecklistData(checklistDataCopy)
} else {
setData(element.value)
}
element.style.height = '0'
element.style.height = element.scrollHeight + 'px'
}
function toggleChecklist() {
setChecklist((prevChecklist) => !prevChecklist)
if (checklist) {
const n = String.fromCharCode(13, 10) //newline character
setData(checklistData.reduce((p, c) => p.concat(c.data + n), ''))
blobRef.current && blobRef.current.dispatchEvent(new Event('input'))
}
}
function dataArea(item?, index?) {
return (
<textarea
id={item ? item.id : 'add-task'}
name="data"
className="input"
value={item ? item.data : data}
onKeyDown={(e) => addChecklistItem(e, index)}
onInput={(e) => handleInput(e, index)}
onFocus={() => setEditing(true)}
placeholder={inputLabel}
rows="1"
ref={item ? (item.id === newItemId ? lastRef : undefined) : blobRef}
// HELP ^^^ blobRef is null after toggling to/from checklist
/>
)
}
...
return (
<>
<form onSubmit={handleSubmit} onBlur={blurCancel}>
<label htmlFor="add-task" className="visually-hidden">
{inputLabel}
</label>
{checklist ? checklistGroup : dataArea()}
{isEditing ? editingTemplate : ''}
</form>
</>
)
On first render the ref is null. Only on the second render will it be populated. This is because during the first render pass, the HTML is not yet loaded, so there is no HTML element reference.
This applies to when you switch to the other component and back to dataArea, as when you switch away react sets it to null when it unmounts since the element reference is gone from the DOM. So when it remounts again, on first render it will be null.
You can sometimes get around this sort of problem with useLayoutEffect which only runs when the DOM has rendered.
Your code is a little odd in that you also are conditionally applying a ref. Normally, you want this to be stable, I would expect it to be simply:
ref={blobRef}
And any branching would be done when you consume the ref, not set it. It will be quite hard to manage otherwise.
However, before I go further I should warn you grabbing the element and setting its height is not actually a good idea. This violates React, because you shouldn't change a DOM nodes properties such that they no longer can be reproduced by React. There is now a different between reality and React's internal VDOM implementation.
This is quite tricky though, I'd recommend just using a library: https://github.com/Andarist/react-textarea-autosize/tree/main/src. These libraries get around the problem by rendering a cloned off-screen text area outside of React's remit and taking the heights from that, then applying them use the style prop on the "real" textarea.
A few notes around your implementation:
On the first render, the ref is null and not undefined because of component mounting or unmounting and setting the state.
Your ref is assigned on further renders.
You are doing DOM manipulation by directly assigning heights to DOM element which is not a correct way of doing it.
You can check by console logging that you are not getting the ref in actual as blobRef.current got nothing/undefined and thus handleInput is not triggered, thats why you added an extra && check there. You can handle the case of undefined too. If you don't get any ref then?
You cannot achieve this directly, either use a package or take the height from actual DOM instead of VDOM and assign on the actual DOM element.
A NPM packages can be helpful in your case as mentioned by #adam-thomas.
Example is: react-textarea-autosize

How can I pause a React-Autosuggest search until after async data is ready?

I have a feature that uses React-Autosuggest to search through a small amount of data, all stored in memory once fetched from the backend. In other words, after the initial bulk fetch, there are no more network calls to power search.
The function I call when the search box calls onSuggestionsFetchRequested naively looks through this.props.dataset to return search suggestions.
<Autosuggest
...
onSuggestionsFetchRequested={({value}) => {this.setState({currentSearchSuggestions: this.getSuggestions(value)});}}
...
/>
getSuggestions(rawValue) {
const toSearchThrough = Object.values(this.props.dataset);
...
}
However, if the user enters a search before this.props.dataset is populated, no suggestions show even after the data loads up, until the user changes their input slightly, causing getSuggestions to be invoked again.
Is there something clever I can do to block getSuggestions until the dataset is ready? Again, this is something that only happens once each page load.
I think this can solve your problem.
componentDidUpdate(prevProps) {
// Assume this.state.searchQuery is set on user input via onChange defined in Autosuggest inputProps
if (this.state.searchQuery && this.props.dataset !== prevProps.dataset) {
this.setState({currentSearchSuggestions: this.getSuggestions(searchQuery)});}
}
}
Can you prevent the component from rendering until there's data? If dataset is an empty object you'll have to check it's length, but something like this:
{this.props.dataset && (
<Autosuggest ... />
)}

Create reference to non loaded preact component

I am trying to change some text in a preact component based on an object the user clicks in this threejs scene. I am using Matt DesLauriers' template for this app as I wanted to see how professionals might do it. This is the first time I have used react(although its actually preact) aswell as threeJS.
In theory my method should work except for one problem. The way Matt DesLauriers set up the loading screen means that the main UI component, named Landing, is rendered after the loading has finished. This achieved by
getContent (section) {
// You are probably better off using a real "Router" for history push etc.
// NB: Ensure there is a 'key' attribute so transition group can create animations
switch (section) {
case 'Preloader': return <Preloader key='Preloader' />;
default:
case 'Landing': return <Landing key='Landing'/>;
}
}
render () {
const classes = classnames({
'App': true
});
const section = this.state.section;
const content = this.getContent(section);
// Render the WebGL if loaded
// And also render the current UI section on top, with transitions
return (
<div className={classes} ref={ c => { this.container = c; } }>
{ this.state.isLoaded && <WebGLCanvas />}
<PreactTransitionGroup className='content' >
{ content }
</PreactTransitionGroup>
</div>
);
}
I need to be able to reference Landing so I can set it's state, however giving it a ref case 'Landing': return <Landing key='Landing' ref={this.ref}/>; results in a reference to an empty object. If I put the Landing component in directly it works exactly as intended.
Any idea how to fix this?
Also here is the public GitHub repo if you want.

How to check if map function is finished

UPDATE: The process of what must happen
User drag and drops multiple pdf files
Each of these pdf files are then visually rendered one by one for the user in a list
As long as the list is still updating, a pop up will come up saying "please wait until everything is loaded"
After all the pdfs are loaded, the user can read the pdf in an embed to quickly check if this is what he wants to upload and can add a title and description for each pdf.
Every pdf has received an item; the user now clicks 'upload'
Brief description
So I have a map function that loads a lot of items with large file size. I want to load each result one by one, but I want to hide or block it using a 'isLoading' state until everything is finished loading.
Question
How do I check if a map function is finished loading everything?
UPDATE: My code
{this.state.pdfSrc!== null ? // IF USER HAS UPLOADED FILES
this.state.pdfSrc.map(item => ( // MAP THROUGH THOSE FILES
// AS LONG AS THE MAP FUNCTION KEEPS LOADING FILES, A POP UP MUST COME UP SAYING "please wait until everything is loaded"
<div key={item.fileResult} className='uploadResultList'>
{item.fileName}
<embed src={item.fileResult} />
<input type='text' placeholder='Add title*' required />
</div>
)) /
:
null
}
But this only gives me following error
Warning: Functions are not valid as a React child. This may happen if
you return a Component instead of from render. Or maybe
you meant to call this function rather than return it.
Instead of doing this in a single function, you can implement the map in your component's render() method:
render() {
return(
<div>
{!!imgSrc ? (
<PlaceholderComponents/>
) : this.state.imgSrc.map(item => (
<div key={item.fileResult} className='uploadResultList'>
<embed src={item.fileResult} />
</div>
}
</div>
)
}
However, you would need to 'load' the file data before the doing so, either using componentDidMount or componentWillMount (which is now being deprecated, so try to avoid using it). For example:
componentDidMount() {
getImageSrcData(); // Either a function
this.setState({ imgSrc: imgFile }) // Or from setState
}
If you want more info on setting state from componentDidMount, see this thread:
Setting state on componentDidMount()
EDIT: You can use the React Suspense API to ensure the file loads before rendering...
// You PDF Component from a separate file where the mapping is done:
render() {
return (
<div>
{this.state.imgSrc.map(item => (
<div key={item.fileResult} className='uploadResultList'>
<embed src={item.fileResult} />
</div>}
</div>
)
}
const PleaseWaitComponent = React.lazy(() => import('./PleaseWaitComponent'));
render() {
return (
// Displays <PleaseWaitComponent> until PDFComponent loads
<React.Suspense fallback={<PleaseWaitComponent />}>
<div>
<PDFComponent />
</div>
</React.Suspense>
);
}
Update: You may need to look at creating a higher order component that can take another component and wait till all of those are loaded and then change the state. Perhaps something similar to react-loadable?
You don't need to pass a callback into the setState. .map isn't asynchronous and once the iteration has finished it will then move to the next line in the block.
The issue you're having here is that you're just calling map and returning JSX in the callback but it isn't going anywhere.
You should call the .map inside the render method. I'm not sure why you're trying to update the isLoading state. I think this should be handled elsewhere.
render() {
cosnt { imgSrc } = this.state;
return (
{
imgSrc.map(item => (
<div key={item.fileResult} className="uploadResultList">
<embed src={item.fileResult} />
</div>
));
}
)
}

Categories

Resources