Prevent React setState from batching updates - reactjs

I'm calling a function createSession when a user clicks on a button.
In createSession I run:
this.setState({divClass: 'show'}, function() {
...computationally intensive and async stuff
})
By changing this.state.divClass to 'show' the DOM is supposed to be re-rendered and show a div that previously had a class of 'hide'
I've dropped a console.log in the render function to make sure that setState is in fact re-rendering before it does the other stuff. Even though the console is showing the component has re-rendered, the view does not change until other stuff finishes.
What I'm confused about is that the component is being re-rendered before the callback is called, but the changes won't display until the callback is finished.
I'm using version 13.0 of React
Here is some code (sorry, it's in coffee):
crypto = require('crypto')
Component = React.createClass({
render: ->
<button onClick={#showDiv}>Show Div</button>
<div className={#state.divDisplay}>
Wooo
</div>
getInitialState: -> {
divDisplay: 'hide'
}
showDiv: ->
#setState({divDisplay: 'show'}, ->
salt = crypto.randomBytes(16).toString('hex')
# this call is not really sync
crypto.pbkdf2('password', salt, 10000, 32, 'sha1', (error, hash) ->
console.log error, hash
)
)
})
UPDATE (and personal solution)
I found that the problem was not that React was batching its updates to the state - I was using a password hashing library that ran synchronously. The library would block the UI from updating. So even though the render call would console.log that the state had changed, the UI was not able to update.
I ended up using a web worker to hash the password in the background. This allowed the rest of the UI to operate as usual.

It seems you have typo in render(). Should be #state.divDisplay instead of #divDisplay.

Related

React/Redux : Detect and exectute instructions before component destruction

Ola,
In my redux state, I have a isEditing boolean value, initiate at false, to manage a profile editing mode. I create a redux action to toggle this state. Below, some screen of the component render.
It works in most cases:
When I click on Edit name, I toggle to true and display the edit form
When I click on Save (or Cancel), it perform (or not) api request, then toggle to false, displaying the initial component
But if I start editing then quit page manualy (via logo link or url), and then come back to this page, obviously, the edit mode is still active at true.
I would like to put my state to false when I leave page, but I don't find any option to dispatch an action before component destroyed (like old componentWillUnmount() class programming method or beforeUnmount VueJS equivalent)
PS : I use react router V6. It seems a hook was implemented during beta but never released, and no news since :(
An alternative is to force value to false on component creation, with useEffect. It works, but I think it's not the right place. I see the edit mode opened then close in my ui during a few ms.
Thanks in advance
Try using the clean up on an useEffect hook
useEffect(() => {
// Specify how to clean up after this effect:
return function cleanup() {
// make redux call to toggle edit mode
};
});
I believe you could dispatch an action in the useEffect return value like useEffect(() => { return () => dispatch(changeEditMode(false)) }, []). It is mostly similar to a componentWillUnmount().

React Hooks API call - does it have to be inside useEffect?

I'm learning React (with hooks) and wanted to ask if every single API call we make has to be inside the useEffect hook?
In my test app I have a working pattern that goes like this: I set the state, then after a button click I run a function that sends a get request to my API and in the .then block appends the received data to the state.
I also have a useEffect hook that runs only when the said state changes (using a dependency array with the state value) and it sets ANOTHER piece of state using the new data in the previous state. That second piece of state is what my app renders in the render block.
This way my data fetching actually takes place in a function run on a button click and not in the useEffect itself. It seems to be working.
Is this a valid pattern? Thanks in advance!
Edit: example, this is the function run on the click of the button
const addClock = timezone => {
let duplicate = false;
selectedTimezones.forEach(item => {
if (item.timezone === timezone) {
alert("Timezone already selected");
duplicate = true;
return;
}
});
if (duplicate) {
return;
}
let currentURL = `http://worldtimeapi.org/api/timezone/${timezone}`;
fetch(currentURL)
.then(blob=>blob.json())
.then(data => {
setSelectedTimezones(prevState => [...prevState, data]);
}
);
}
Yes, apis calls that happen on an action like button click will not be part of useEffect call. It will be part of your event handler function.
When you call useEffect, you’re telling React to run your “effect”
function after flushing changes to the DOM
useEffect contains logic which we would like to run after React has updated the DOM. So, by default useEffect runs both after the first render and after every update.
Note: You should always write async logic inside useEffect if it is not invoked by an event handler function.
Yes, you can make api requests in an event handler such as onClick.
What you don't want to do is make a request directly inside your functional component (since it will run on every render). As long as the request is inside another function and you only call that function when you actually want to make a request, there is no problem.

React: Append component animations on local and redux state updates

I am building a component that renders an animation on every local and redux state update. Local state is updated on a button click and it also makes an ajax request to a remote server to update that an user clicked the button. I update local state on every click like so:
const [selfAnimations, setAnimations] = useState<AnimationRenders>(initialAnimationState);
const onButtonClickCallback = () => {
makeRemoteAjaxCall();
selfAnimations.localEventsCount += 1;
selfAnimations.animationsToRender.push(Date.now);
setAnimations(selfAnimations);
};
I update the data coming in from remote server like this:
useEffect(() => {
if (animationDataCount && animationDataCount > 0) {
const {
updatedLocalEventsCount,
updatedAnimations,
} = filterDedupedLocalEvents(selfAnimations, animationDataCount);
setAnimations({
localEventsCount: updatedLocalEventsCount,
animationsToRender: updatedAnimations,
});
}
}, [animationDataCount]);
I render a div with a component I created:
<div classNames={...}>
<AnimationContainer numberOfAnimations={selfAnimations.animationsToRender.length} />
</div>
<div className={...}>
<MyButton ... />
</div>
On client side, data is received on regular intervals. My AnimationContainer component can render any number of received animations. However, everytime a user clicks the button or client recieves remote data it updates the state and drops previous animations. Looking online I could delay the rerender with css but the behavior I need is to append to existing animations on state updates. Is there a mechanism that I could use for that behavior?
I misunderstood how react renders. React does not necessarily create a new child component and unmount the old component on props/state updates. In my case, instead of storing counts of what needed to be rendered, I need to store distinct keys for what needed to be rendered, because even if the AnimationContainer component receives updates from props, if it only received counts, then it may not trigger rerenders because the count can be the same. I got around this by implementing the props of localEvents as an array of timestamps: number[]. By passing down timestamps to AnimationContainer and refactoring AnimationContainer to check props update and store into state of animations in middle of rendering and a queue of animations to render, I was able to create a component that can receive and continually renders animations based on events.

Will ReactDOM.hydrate() trigger lifecycle methods on the client?

From the React 16 docs about ReactDOM.hydrate(),
Same as render(), but is used to hydrate a container whose HTML contents were rendered by ReactDOMServer. React will attempt to attach event listeners to the existing markup.
Will ReactDOM.hydrate() also trigger lifecycle methods on the client such as componentWillMount(), componentDidMount() during initial render?
Will render() method be called on the client during hydration? I suppose not, because that's the difference between ReactDOM.render() and ReactDOM.hydrate()?
If render method won't be called on the client, we wouldn't expect componentDidMount() lifecycle method to be triggered.
If none of the lifecycle methods are called on the client, how would we know when has React finished rendering. I suppose the callback in the following syntax:
ReactDOM.hydrate(element, container[, callback])
I want to understand if there are lifecycle methods / hooks (which give more control over the application) available when React is "attempting to attach event listeners to existing markup".
Since ReactDOM.hydrate is (and should be) called on the client then YES it is supposed to run componentDidMount. componentWillMount is already called when rendered on the server.
componentDidMount does not run on the server, therefore when you call hydrate, the app runs the event.
Think about hydrate as a different render method. It does render but not in the same way. It looks for mismatches between your server rendered React and your client React. It does not render everything again.
React expects that the rendered content is identical between the server and the client. It can patch up differences in text content (such as timestamps), but you should treat mismatches as bugs and fix them
However you might want to do some crazy stuff like rendering something completely different on the client side (than what was rendered on the server). For this pay attention to this paragraph
If you intentionally need to render something different on the server and the client, you can do a two-pass rendering. Components that render something different on the client can read a state variable like this.state.isClient, which you can set to true in componentDidMount(). This way the initial render pass will render the same content as the server, avoiding mismatches, but an additional pass will happen synchronously right after hydration. Note that this approach will make your components slower because they have to render twice, so use it with caution.
So as you can see it does a render pass. If there are no mismatches React is optimized for that.
I hope it was clarifying. I speak from experience with React SSR and basic understanding of reading the docs.
The rendered elements probably aren't same between server and client, because initially the elements are rendered into texts at the server in memory, therefore they are not mounted. When the content is moved to client, it can be re-attached to react via hydrate which is fake "render" to wire with the rest of react functionalities, such as events.
In order to tell when it's hydated, here's a piece from internet which I found clearly stated the above rational. https://dev.to/merri/understanding-react-ssr-spa-hydration-1hcf?signin=true
const HydrateContext = createContext('hydrated')
export function useIsHydrated() {
return useContext(HydrateContext)
}
export function IsHydratedProvider({ children }) {
const [isHydrated, setIsHydrated] = useState(false)
useEffect(() => {
setIsHydrated(true)
}, [])
return (
<HydrateContext.Provider value={isHydrated}>
{children}
</HydrateContext.Provider>
)
}
To use it,
function MyComponent() {
const isHydrated = useIsHydrated()
return !isHydrated ? 'Initial render' : 'SPA mode'
}
function App() {
return (
<IsHydratedProvider>
<MyComponent />
</IsHydratedProvider>
)
}
It feels to me, any rendered component teleports from the server to the client.
p.s Here's another article which talks about the second render after the mount, https://medium.com/swlh/how-to-use-useeffect-on-server-side-654932c51b13
I read the type of ReactDOM.hydrate in TypeScript system:
(
element: SFCElement<any> | Array<SFCElement<any>>,
container: Container| null,
callback?: () => void
): void;
And example to the above declaration:
ReactDOM.hydrate(
<App />, // element
document.getElementById('root'), // container
() => { // callback
/* do what you want after hydration */
}
);

How to prevent route change using react-router

There's a certain page in my React app that I would like to prevent the user from leaving if the form is dirty.
In my react-routes, I am using the onLeave prop like this:
<Route path="dependent" component={DependentDetails} onLeave={checkForm}/>
And my onLeave is:
const checkForm = (nextState, replace, cb) => {
if (form.IsDirty) {
console.log('Leaving so soon?');
// I would like to stay on the same page somehow...
}
};
Is there a way to prevent the new route from firing and keep the user on the same page?
It is too late but according to the React Router Documentation you can use preventing transition with helping of <prompt> component.
<Prompt
when={isBlocking}
message={location =>
`Are you sure you want to go to ${location.pathname}`
}
/>
if isBlocking equal to true it shows a message. for more information you can read the documentation.
I think the recommended approach has changed since Lazarev's answer, since his linked example is no longer currently in the examples folder. Instead, I think you should follow this example by defining:
componentWillMount() {
this.props.router.setRouteLeaveHook(
this.props.route,
this.routerWillLeave
)
},
And then define routerWillLeave to be a function that returns a string which will appear in a confirmation alert.
UPDATE
The previous link is now outdated and unavailable. In newer versions of React Router it appears there is a new component Prompt that can be used to cancel/control navigation. See this example
react-router v6 no longer supports the Prompt component (they say that they hope to add it back once they have an acceptable implementation). However, react-router makes use of the history package which offers the following example for how to block transitions.
Note that to actually make this work in react router you have to replace the createBrowserHistory call with some hackery to make sure you are using the same history object as react router (see bottom of answer).
const history = createBrowserHistory();
let unblock = history.block((tx) => {
// Navigation was blocked! Let's show a confirmation dialog
// so the user can decide if they actually want to navigate
// away and discard changes they've made in the current page.
let url = tx.location.pathname;
if (window.confirm(`Are you sure you want to go to ${url}?`)) {
// Unblock the navigation.
unblock();
// Retry the transition.
tx.retry();
}
You'll need to put this inside the appropriate useEffect hook and build the rest of the functionality that would have otherwise been provided by prompt. Note that this will also produce an (uncustomizable) warning if the user tries to navigate away but closing the tab or refreshing the page indicating that unsaved work may not be saved.
Please read the linked page as there are some drawbacks to using this functionality. Specifically, it adds an event listener to the beforeunload event which makes the page ineligable for the bfcache in firefox (though the code attempts to deregister the handler if the navigation is cancelled I'm not sure this restores salvageable status) I presume it's these issues which caused react-router to disable the Prompt component.
WARING to access history in reactrouter 6 you need to follow something like the instructions here which is a bit of a hack. Initially, I assumed that you could just use createBrowserHistory to access the history object as that code is illustrated in the react router documentation but (a bit confusingly imo) it was intended only to illustrate the idea of what the history does.
We're using React Router V5, and our site needed a custom prompt message to show up, and this medium article helped me understand how that was possible
TLDR: the <Prompt/> component from react-router-dom can accept a function as the message prop, and if that function returns true you'll continue in the navigation, and if false the navigation will be blocked
React-router api provides a Transition object for such cases, you can create a hook in a willTransitionTo lifecycle method of the component, you are using. Something like (code taken from react-router examples on the github):
var Form = React.createClass({
mixins: [ Router.Navigation ],
statics: {
willTransitionFrom: function (transition, element) {
if (element.refs.userInput.getDOMNode().value !== '') {
if (!confirm('You have unsaved information, are you sure you want to leave this page?')) {
transition.abort();
}
}
}
},
handleSubmit: function (event) {
event.preventDefault();
this.refs.userInput.getDOMNode().value = '';
this.transitionTo('/');
},
render: function () {
return (
<div>
<form onSubmit={this.handleSubmit}>
<p>Click the dashboard link with text in the input.</p>
<input type="text" ref="userInput" defaultValue="ohai" />
<button type="submit">Go</button>
</form>
</div>
);
}
});

Resources