React/Redux : Detect and exectute instructions before component destruction - reactjs

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().

Related

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 with Redux Update only state

I'm working on integrating Redux in an already finished SPA with ReactJS.
On my HomePage I have a list of the 4 newest collections added which on render, I fetch with axios from my database. These are then saved in Redux Store and displayed on the React UI.
My mapStateToProps look something like this:
const mapStateToProps = (state) => ({
credentials: credentials(state),
collections: collections(state)
});
Where credentials is irrelevant and collections is:
const collections = (state) => {
if (state.collectionsHomeViewReducer.fetching === true) {
return {
fetchingCollections: true
}
}
else if (state.collectionsHomeViewReducer.data) {
const response = state.collectionsHomeViewReducer.data;
return {
collections: response.collections,
fetchedCollections: true,
fetchingCollections: false
}
}
else if (state.collectionsHomeViewReducer.fetched === false) {
return {
fetchedCollections: false,
fetchingCollections: false
}
}
};
What is it I want to do:
Update the store state every time another client, or the current client, adds a new collection. Moreover, I do not wish for the UI to update immediately after I dispatch(action), I want it to update when a user refreshes the page or when he navigates to another view and returns ( I believe what I'm trying to say is when componentDidMount is called ).
What have I achieved so far:
By using socket.io, I
socket.emit("updateCollectionsStore")
socket.on("updateCollectionsStore")
and
socket.broadcast.emit("updateCollectionsStore")
in their respective places in the application. The final call of
socket.on("updateCollectionsStore")
after the broadcast, is in the main file of the page, app.jsx where the store is also located. The function there looks like this:
socket.on("updateCollectionsStore", () => {
store.dispatch(getCollectionsHomeView());
});
The store is updated and everything works fine, as viewed from the Redux Dev Tools.
What I can't seem to figure out is to tell the props not to change due to the fact that mapStateToProps is called every time an action is dispatched.
Why do I need this: The HomePage can deal with a continuous UI update and data fetching but I also have a page ReadAllPage where you can real all collections. The problem is if there will always be the newest post on the top, whenever a new one is added, the current one is pushed downwards. In case somebody had the intent to click the one that was pushed down, now he might have accidentally clicked the one that took its place, which is not wanted.
What else should I do different or further to achieve the result I want?
Thank you.
According to your needs, I would have two properties in the state. First is that is currently visible on the HomeView and the second is that is updated via sockets. Once a user navigates to the HomeView you can just replace the first collection with the second one.

Manually update DraftJs ContentState with clicked text

How can I manually update DraftJs's ContentState in response to clicked text?
I have a list of text item. When one is clicked I am passing that text down to Draftjs, but because I am setting the state using componentWillReceiveProps() it requires that I click the text twice to get an update.
componentWillReceiveProps() {
const activeNoteText = this.props.activeNoteText;
if (activeNoteText !== '') {
this.setState({ editorState: EditorState.createWithContent(ContentState.createFromText(activeNoteText)) });
}
}
First click: Update the App state and pass props down to Draftjs (component updates before receiving new props)
Second click: Now the prop is properly set and Draftjs updates (component updates with the props received on the first click)
How can I accomplish this in one pass? I know there's no componentDidReceiveProps and I know there's a good reason, though I can't claim to fully understand yet, so what's the best practices way to accomplish something like this?
Why are you using componentwillReceiveProps?.
What you can do, is have the states your setting in Draftjs i.e. editorState(Well, that's what I can make out) in its parent and whenever a list item is clicked on the click handler for that update the editorState and then pass it as props to Draft js.
Further for the condition, where you are checking if it is not empty,
You could use the
getInitialState(){
.....
}
For initialization when your component is initially loaded. So you could have a default value for editorState.

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>
);
}
});

Prevent React setState from batching updates

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.

Resources