Why won't my app render when state changes? - reactjs

I'm building an application that allows for posting community concerns with the ability to upvote those concerns using React, and right now, I am working on the upvote functionality. One way I'm trying to limit the currently logged in user to a single vote is to disable the button once an upvote has been successfully registered for any given post.
To do this, I created a function that checks if the logged in user's ID matches the ID of the upvote for each post. If no match is found, this means the user hasn't voted for already and can register the new upvote. Once this is complete, the button is disabled. I created state for this and is set to false upon the initial render (not sure if this is what I should be doing). I also created state for the all of the votes that have been successfully registered. Both are included below.
const [alreadyVoted, setAlreadyVoted] = useState(false);
const [userVotes, setUserVotes] = useState(upvotes) // upvotes is being passed via props
I'm using the useEffect hook (again, not sure if this is the best way) to check if each button should be enabled or disabled like so:
useEffect(() => {
hasVoted()
}, [userVotes])
Finally, my hasVoted function checks to see if the user has already voted for the issue before and determines the state. It looks like:
function hasVoted() {
userVotes.forEach(vote => {
if (vote.issue_id === issue.id) { // issue here is from props
setAlreadyVoted(true) // I want this to then disable my button
}
})
}
Right now, when I click the button to register the upvote, the page doesn't rerender upon clicking. However, if I refresh the page, the button is successfully disabled as it should be. It probably goes without saying, I'm still getting the hang of React, but any and all help is appreciated.

This code is meaningless at first place
userVotes.forEach(vote => {
if (vote.issue_id === issue.id) { // issue here is from props
setAlreadyVoted(true) // I want this to then disable my button
}
})
, it can be transformed into
if (userVotes.some(vote => vote.issue_id === issue.id)) setAlreadyVoted(true)
but still the logic of it is not clear

I think hasVoted function should be in the useEffect scope. If it's outside, it might introduce bugs, as documented here: https://reactjs.org/docs/hooks-faq.html#is-it-safe-to-omit-functions-from-the-list-of-dependencies
You could try if this helps to resolve your problem

you seem to be somewhat on the right track here but your problem may lay elsewhere,provided the props are passed correctly,because the application does re-render when the state changes. if you want to just disable button you can just use simple html attribute to do that
{alreadyVoted ? (
<button class="btn btn-success" disabled="disabled">
Upvote
</button>
) : (
<button
class="btn btn-success"
onClick={() =>
// Add the id to the setUserVotes here
setAlreadyVoted(true)}
>
Upvote
</button>
)}
Take a look at this codesandbox that i just created and get back to me if you have any issues still

Related

Prevent navigation until React form is validated

I'm trying to implement a validation functionality in React wherein the user will fill in a form, and then the user can navigate to other tabs of the application.
What i want to do is disabling the navigation until certain parts of the form has been filled, if the user attempts to change the tabs without filling said fields, a message will appear directly on the page to alert the user that the fields are required.
My current approach is to capture the event prior to the page switch, check for the form's validity, and then decide whether or not the user can navigate based on said.
The problem is I have no idea which event to capture or how to prevent user's navigation, can someone help me?
EDIT:
Here's what i've tried so far:
componentWillUnmount() {
this.handleValidation();
}
handleValidation = () => {
if (this.invalidForm()) {
this.setState({ showValidation: true });
this.preventNavigation();
}
};
preventNavigation = () => {
window.location.reload();
}
render()
return (
///Form information...
{this.state.showValidation && (
<div>
You must provide all required information
</div>
)}
)
I've thought that by using location.reload in componentWillUnmount(), the page would be reloaded before the navigation (hence, making the user stay in the old page), but what really happens is that it navigates to the other tab, then proceeds to reload there.

Show warning on leaving without saving

I'm working on a React page which has a card component which opens on clicking a button. I'm trying to show a warning if the user tries to close the card without saving the changes. The card doesn't have a close button, it closes when clicking anywhere on the screen outside of the card.
I've built a similar warning modal by checking if the route has changed, however since in this case the card component is part of the same page I cannot apply the same logic.
<CardSidebar
onHide={(e) => this.setState({ showSidebar: false})}
>
<FormComponent
data={this.state.item}
filterTypes={this.state.filterTypes}
dataFields={stores.dataFieldStore.dataFieldsForDropDownComponents}
refresh={this.refreshListHandler}
cancelHandler={this.cancelHandler}
/>
<>
<RouteLeavingGuard
// when={?}
/>
</>
</CardSidebar>
So basically you want to know when the form is dirty and data is not saved so the guard would pop up. As far as I can see you are not using any library for handling this kind of behavior in forms so you need to build custom. I also don't see that you are using something as Redux so you need to lift state up and keep the isDirty value in the state of the component that is shown in the snippet.
//This code goes in the snippet that you pasted
state={
isDirty:false
}
isDirtyHandler(value){
this.setState({isDirty:value})
}
Pass the isDirtyHandler and isDirty as prop for check into the <FormComponent/> and inside the component make the following check
//This goes in <FormComponent/>
componentDidUpdate(prevProps, prevState) {
if(this.state.data !== prevState.data && !this.props.isDirty) {
this.props.isDirtyHandler(true)
}
onSubmitHandler(){
this.props.isDirtyHandler(false)
//whole logic for submitting the form
}
and just in the guard you are checking if form is dirty
<RouteLeavingGuard
popUp={isDirty}
/>

How to use a method in the Main component on another

I am trying to save notes into localStorage (or in this case localforage). I have a simple text area with a button called "save." The save button is located in another file indicated below.
I used an example found here to try to set the items.
This is the code I wrote:
SaveMessage() {
var message = <Notepad></Notepad>;
reactLocalforage
.SetItem("Message", message, function(message) {
console.log(message);
})
.catch(function(err) {
console.log(err);
});
}
The var message is something I'm not too sure of either. Notepad is another component with the code which contains the text area and buttons:
<div className="button-container">
<button
className="save-button"
onClick={() => {
props.onSaveMessage(saveMessage);
}}
>
Save
</button>
<button className="remove-button">Show all</button>
</div>
The area where it says onClick I was hoping there would be a way to use the SaveMessage method with localforage initially I tried creating it as a prop (from a tutorial) so in the main method I'd have:
render() {
return (
<div>
<Notepad onSaveMessage={this.SaveMessage}></Notepad>
</div>
);
}
and then on the Notepad component:
<button
className="save-button"
onClick={() => {
props.onSaveMessage();
}}
>
Save
</button>
When I click the save button on my application I am hoping something will be set within the local-storage on the browser, but I get an exception:
TypeError: Cannot call a class as a function
The error occurs when I set item on the save message code above and when I try to call it as a prop onSaveMessage(saveMessage).
You haven't added enough code to be able to fix your exact issue, but I can help you understand how you should proceed to fix it. In React when you want to share information between components, you need to lift that data into a parent component, and then share it through props. If your component tree is profound, then this task can get tedious. So several libraries have been developed to manage your app's state.
I suggest you start by reading "Lifting State Up" from the React docs page. To help you apply these concepts to your current situation, I've created a CodeSandbox in which I try to replicate your situation. You can find it here.
Once you understand the need to "lift" your state, and how you can share both data and actions through props you can migrate to state handler tool. Some of them are:
React Context
Redux
MobX
There are much more. It is not an extensive list, nor the best, just the ones I have used and can vouch that they can help you.
I hope this helps.

reactjs -- handle checkbox (multiple) click events

I know there are some similar topics but none seems to be in the same direction of what I'm trying to do, thus a new thread.
I have a component that displays a list of keys, each with a checkbox attached to the string. In addition, I have a button that supposedly calls an API with all keys selected and delete these keys.
Several things I'm trying to achieve:
checking a check box enables the delete button
click the delete button should send a POST to API, the list should then reload
Since the list is reloaded, all checkbox should be unselected, thus the delete button is once again disabled
there's another button outside of this function that checks for the length of the list as well, which I don't know how to associate with this list if I fetch the list in the component.
I'm facing the problem which I don't know how to make the button and the checkboxes associate to each other. I tried using state with a checked state, which is a boolean, but that's only one boolean and cannot record several keys. I think using an array would work? Then again I'm not sure how to properly append or remove the key checked.
my code looks like
class AppList extends Component {
constructor() {
super();
this.state = {
checked: [],
apps: []
};
this.handleChecked = this.handleChecked.bind(this);
}
componentDidMount() {
fetch("some_url", {
method: 'post',
body: JSON.stringify({"user": "some_email"}),
headers: {'Content-Type': ' application/json'}
})
.then(res => res.json())
.then(
(result) => {
this.setState({apps: JSON.parse(result)});
},
(error) => {
console.log("error", error);
}
);
}
handleDeleteKey = (event) => {
// fetch(I'll worry about this later)
this.setState({checked: false});
console.log("delete!!!!");
}
handleChecked () {
this.setState({checked: !this.state.checked});
}
render () {
const apps = this.state.apps.map((app) =>
<div>
<input type="checkbox" onChange={this.handleChecked} />
{` ${app}`}
</div>
);
return (
<div>
<h4>Client Key List:</h4>
{this.state.apps.length > 0 ? <ul>{apps}</ul> : <p>No Key</p>}
{this.state.apps.length > 0 ? <button className="api-instruction-button" onClick={this.handleDeleteKey}>Delete Selected Key(s)</button> : null}
</div>
);
}
}
export default AppList;
I feel like my design is completely wrong but I don't know how to fix it. It seems like there are so many states to be passed around and nothing is the outermost, almost a cyclic dependency.
Anyone had any experience dealing with this problem? It seems like it's a common user action but I can't figure it out.
EDIT: after digging it a bit more, it seems like I need to call componentDidMount outside of the AppList. It should be in the component that uses AppList, let's call it MainApp.
MainApp calls componentDidMount which is the same as the one in AppList. The one in AppList gets removed, and the keys are passed to AppList as props.
I have trouble handling the clicking event. It seems like the component is always updating, so if I want to append the clicked key to the array, it wouldn't work. The same call will be made again and again.
Since there's another button in MainApp that requires the list of keys, I can't just pass the call into AppList. However, updating in AppList should update the MainApp as well. How does it work? I'm so confused
EDIT2:
https://codesandbox.io/s/7w2w11477j
This recreation should contain all functions I have so far, but I can't get them to work together.
Again my task is simply:
I have a list of strings, each with a checkbox
checking the checkbox selects the specific string
There's a button that I can click to delete these entries in my db by calling an API
Is refreshing the MainApp needed in this case? Otherwise I need to delete the strings in frontend so they don't display after the delete button is pressed
Here's what I believe you were going for: https://codesandbox.io/s/w23wv002yw
The only problem that made yours not work properly was you were just getting a little jumbled with where to put everything.
Contents:
The MainApp.js will only contain the apps and a method for deleting them in the backend. Other than those two methods, nothing else really concerns the MainApp.js file.
The AppList.js will contain all the methods that update its own checked state, the delete button itself, and a method to clear the checked state on delete.
Processes:
First, MainApp.js will load and remount with a backend api pull and populate its apps state. Once it's finished that, it will pass it on to AppList.js. From there, AppList.js will render that list as a multi-select field onscreen. The user can then select or deselect any of the options. As an option is selected, its index is pushed to the checked state and organized in ascending order.
(ordering the array isn't that necessary, but I figured it would help if you wanted to retool it sometime down the road)
When one or more option is selected, a delete button will appear. When the user clicks the delete button, AppList.js will call the delete function passed to it from MainApp.js, then it will clear the current checked state.

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