ReactJS/GraphQL: Call query from submit button - javascript

When my app loads the query below runs and the result from the DB is displayed in the browser. However, my app also has a submit button. How can run this entire component when the submit button is pressed while passing the timestamp input from the submit?
handleSubmit(event) {
event.preventDefault();
console.log(this.state.inputValue)
this.state = {
inputValue: new Date(document.getElementById("time").value).valueOf()
};
console.log(this.state);
}
This is the UserList component code:
const UserList = props => (
<Query
query={gql`
query action($timestamp: Float!) {
action(timestamp: $timestamp) {
action
timestamp
object {
filename
}
}
}
`}
>
{({ loading, error, data }) => {
if (loading) return <p>Loading...</p>;
if (error) return <p>Error</p>;
return (
<Item.Group divided>
{data.action.map(action => (
<div>
<ul>
<li>{action.action}</li>
<li>{action.timestamp}</li>
<ul>
{action.object.map(obj => {
return <li>{obj.filename}</li>;
})}
</ul>
</ul>
</div>
))}
</Item.Group>
);
}}
</Query>
);
export default UserList;

Parent component contains submit button and there lives state. Submit handler should only set value in state with ... setState() - NOT directly!
If there is no default state/value use conditional rendering ... in parent render display some placeholder when state (timestamp) is undefined. If value exists pass it as prop:
{!this.state.inputValue ? <SomePlaceholder />
: <UserList timestamp={this.state.inputValue} />}
// place input, submit button where you want (after/before results)
UserList will be called with props - 'props.timestamp' can be used in graphql query literal.
When input changes again submit handler will change parent state and that change will be passed again as prop to child, query fired, results rerendered.

Base on the information provided, there's probably several ways to accomplish what you need. I would say the UserList component needs to conditionally render the Query component in some manner. Perhaps UserList takes a boolean prop isSubmitted and while isSubmitted is false, UserList renders a div with text explaining to submit the query. This could be done as the UserList and the submit button being hosted by the same parent component that has state containing a isSubmitted property that changes on click.

Related

Passing information from child component to parent

This is a follow up question to this question:
Call child method from parent
I am using React > 16.8 with function components and hooks.
I got a parent component which manages a component to display items and add a new item. The list of items is at the parent component. The way the items are added are by a "+" button which opens a new modal window (which is a component of its own), and inside there's a form that the user can insert the details of the new items.
const registerFormRef = useRef();
<Modal
isOpen={isFormOpen}
onCancel={() => setIsFormOpen(false)}
onSubmit={() => { registerFormRef.current.onSubmitForm(); setIsFormOpen(false) }}
titleText="Register Tenant">
<AddItem onAddItem={AddNewItem} ref={registerFormRef}></RegisterTenant>
</Modal>
The AddNewItem is a callback which adds the new item to the list. The modal has an "OK" button which serves as a submit button. It belongs to the parent modal component, not the AddItem child.
The method in the child component:
useImperativeHandle(ref, () => (
{
onSubmitForm()
{
setIsLoading(true);
const newItem = {
name: formSettings["name"].value,
description: formSettings["description"].value,
area: "",
category: ""
}
props.onAddItem(newItem);
setIsLoading(false);
}
}));
I had an issue of getting the information from the child component which holds the form to the parent component, since the submit button as I said, belongs to the modal, I had to somehow call the callback from inside the child form. I have used the accepted answer in the linked question above. It works, but the comment says it's not a good practice passing information like that. Is there another way of passing the information from the child form to the parent component?
The correct way is to store the form data in the parent i.e the component rendering the modal. To do that you could define a state and provide an onChange handler to it. Once you do that on any change in input the AddItem component must notify its parent by calling the onChange method
const App = () => {
const [data, setData] = useState();
const handleChange=(newData) => {
setData(newData);
}
const onSubmit = () => {
// use data to do whatever you want with the formData
console.log(data);
setIsFormOpen(false)
}
...
return (
<Modal
isOpen={isFormOpen}
onCancel={() => setIsFormOpen(false)}
onSubmit={onSubmit}
titleText="Register Tenant">
<AddItem onAddItem={AddNewItem} handleChange={handleChange} ref={registerFormRef}></RegisterTenant>
</Modal>
)
}

React form can be submitted twice despite conditional render

I have a React application coupled to Redux. There is a component rendering a form wrapper (a custom implementation of Formik), while the form inputs themselves are rendered by a child component.
(Not the exact code, but gets the point across.)
...
render() {
const {
config,
updateContactDetails,
errorMessages,
contactDetails,
previousFormValues,
isUpdating,
} = this.props;
const { apiBaseUrl, fetchTimeout, globalId } = config;
const initialValues = previousFormValues || getInitialContactDetailsValues(contactDetails);
if (isUpdating) return <Spinner />;
return (
<Form
initialValues={initialValues}
validate={(values) => validate(values, errorMessages)}
onSubmit={(values) => {
updateContactDetails(apiBaseUrl, globalId, values, fetchTimeout); // dispatch action
}}
>
<ContactDetailsForm content={content} />
</Form>
);
}
...
When you click the submit button in ContactDetailsForm, the value of isUpdating in the Redux store is set to true. As you can see above, that causes the the form to be replaced with a spinner component. However, it is somehow possible to submit the form twice by clicking the button twice.
How can this be? Could there be re-render happening before the one that replaces the form with the spinner? I know I can solve the problem by passing isUpdating into ContactDetailsForm and using it to disable the button, but I still want to illuminate the cause.
EDIT
The reducer looks something like this, in case it helps:
case UPDATE_CONTACT_DETAILS_START: {
return {
...state,
errorUpdatingContactMethods: {},
hasUpdatedContactDetails: false,
isUpdating: true,
contactDetailsValues: action.values,
};
}
You should instead set a disabled property on the button based on the isUpdating prop. It might be that it's just a race condition.

How to setState from within a mapped item within React component

I'm trying to change the state of a component that is part of a mapped array of objects from a json file. I want to ONLY change the item containing the clicked button and none of the others.
I've been attempting to set a property (projectID) with an onClick and while I can get it to toggle one element of state (expanded or not expanded), it does it to ALL the returned results. So I've been trying to get the projectId (set in the data) and use that to set a conditional. But I can't seem to get projectId to update with the click. I briefly played around with context but I think there's something simpler I'm missing. I've attempted it within the onClick (as shown) and from within onViewChange, but that didn't seem to work as I can't access item.id from outside the mapped item.
I'm using a conditional based on a prop handed down from a couple levels up to set the category, showing only the objects I want. That part seems to be working. So I removed my expanded state as it's not necessary to the issue here.
import React from 'react';
import projects from '../data/projects.json';
class ProjectCard extends React.Component {
constructor(props) {
super(props);
this.state={
projects,
expanded: false,
projectId: -1
}
}
onViewChange = (e) => {
this.setState({
expanded: !this.state.expanded
});
console.log(this.state.projectId)
};
render() {
return (
<React.Fragment>
{this.state.projects.map((item) => {
if (!this.state.expanded && item.category === this.props.category) {
return (
<div key={item.id} className="project-card">
<div className="previewImg" style={{backgroundImage: `url(${item.previewImg}` }} ></div>
<div className="copy">
<h2>{item.title}</h2>
<p>{item.quickDesc}</p>
</div>
<div className="footer">
{item.tools.map((tool) => {
return (
<div key={tool} className="tools" style={{backgroundImage: `url(${tool}` }}></div>
);
}
)}
<button onClick={() => this.onViewChange({projectId: item.id})} className="btn float-right"><i className="fas fa-play"></i></button>
</div>
</div>
);
}
}
)}
</React.Fragment>
);
};
};
export default ProjectCard;
I've set a console log to tell me if projectId changes and it always comes back as my default value (-1). The pasted version is the only one that doesn't throw errors with regards to undefined values/objects, but still no changes. If I can get projectId to change based on the item.id from the clicked button, I think I can figure out the conditional and take it from there.
You aren't actually setting the state with the new projectId in your click handler. First step is just simply passing the item's ID to the click handler, not an object:
onClick={() => this.onViewChange(item.id)}
And second part is to actually use that argument in your click handler:
onViewChange = (id) => {
this.setState({
expanded: !this.state.expanded,
projectId: id
});
};
Also, setState() is asynchronous so you can't do what you did and console.log your state on the next line and expect it to have changed. Log the state at the top of your render function to see when it changes (after state/props change, render is called). Or another option is to use the optional second argument of setState, which is a callback that's executed with the new state:
this.setState({id: 5}, () => console.log(this.state.id)) <-- id will be 5

ReactJS - How to properly initialize state from props which is populated by fetching data?

Here is my editing component:
class EditField extends React.Component {
constructor(props) {
super(props);
this.state = { value: '' };
}
edit(e) {
this.setState({ value: e.target.value });
if (e.keyCode === 13) {
this.props.onEdited(this.state.value);
}
}
render() {
return (
<div>
<input
type="text"
value={this.state.value}
onChange={this.edit.bind(this)}
/>
</div>
)
}
}
I need to populate state from props like this:
function Container({ entity, onEdited }) {
return (
<div>
<EditField onEdited={onEdited} value={entity.firstName} />
<EditField onEdited={onEdited} value={entity.lastName} />
</div>
);
}
The Container component get onEdited and entity props from redux store.
Container's parent will handle data fetching and onEdited (which will
only be triggered if user hit Enter) will dispatch request to the server.
My problem is how to initialize value props properly? Because if I use:
componentDidMount() {
this.setState({
value: this.props.value
});
}
I got empty state because fetching data is not finished when componentDidMount
called. And if I use:
componentWillReceiveProps(nextProps) {
this.setState({
value: nextProps.value
});
}
I got this warning:
Warning: EditField is changing a controlled input of type text to be
unncontrolled. Input elements should not switch from controlled to
uncontrolled (or vice versa). Decide between using a controlled or
uncontrolled input element for the lifetime of the component.
So, how to do this correctly?
This is what I recommend:
You could use getInitialState from EditField to populate the value state from the value prop. But this won't work, because getInitialState will only be called once, so subsequent renders will not update the state. Besides, this is an anti-pattern.
You should make the EditField component controlled. Always pass the current value as prop and stop dealing with state at all. If you want a library to help you link the input state with Redux, please take a look at Redux-Form.
The onEdited event you created, at least the way you did it, doesn't play well with controlled inputs, so, what you want to do is to have an onChange event that is always fired with the new value, so the Redux state will always change. You may have another event triggered when the user hits enter (e.g onEnterPressed), so you can call the server and update the entity values. Again. Redux-Form can help here.
Apparently entity.firstName and entity.lastName can only contain the values that the user has confirmed (hit enter), not temporary values. If this is the case, try to separate the state of the form from the state of the entity. The state of the form can be controlled by Redux-Form. When the user hits enter, you can trigger an action that actually calls the server and updates the state of the entity. You can even have a "loading" state so your form is disabled while you're calling the server.
Since Container subscribes to Redux store, I suggest make the EditField stateless functional component. Here's my approach:
const EditField = ({
onEdited,
value
}) => (
<div>
<input
type="text"
value={value}
onChange={onEdited}
/>
</div>
);
class Container extends React.Component {
constructor(props) {
super(props);
this.state = {value: ''};
}
edit = (e) => {
this.setState({value: e.target.value});
e.keyCode === 13 ? this.props.onEdited(this.state.value) : null;
};
sendValue = (val) => val ? val : this.state.value;
render() {
this.props = {
firstName: "Ilan",
lastName: null
}
let { firstName, lastName, onEdited } = this.props;
return (
<div>
<EditField onEdited={this.edit} value={this.sendValue(firstName)} />
<EditField onEdited={this.edit} value={this.sendValue(lastName)} />
</div>
)
}
}
ReactDOM.render(<Container />, document.getElementById('app'));
A live demo: https://codepen.io/ilanus/pen/yJQNNk
Container will send either firstName, lastName or the default state...

Strange behaviour with Material-UI Text Field and Redux state trying to set default value?

I am trying to set a defaultValue property for a Text Field by getting the value from my Redux state however it is not updating accordingly.
I have passed the value as a prop from the container component down to my edit component like so:
render() {
const {data} = this.props
return (
<editcomponent value={this.props.data.value}
)
}
const mapStateToProps = (state) => {
return {
data: state.dataReducer
}
}
In my edit component I tried to just display it first and this works fine:
render() {
return (
<h3>this.props.value</h3>
)
}
When I reload the page with new data in my Redux state it updates accordingly. However, when I try the exact same thing except with a Text Field in which I am setting defaultValue it does not update.
This doesn't work:
render() {
return (
<TextField id="textfield_id" defaultValue={this.props.value}/>
)
}
It will work the initial time, and then when I reload the page with new data it doesn't set the defaultValue to the new data it stays the same as it originally was. If I use value instead then it will change the data but it won't allow me to edit the text box value anymore.
How can I solve this? I want to be able to have a defaultValue set by my Redux state and allow the value to be changed as the user changes/deletes what's in the text box.
I've the same problem and I fixed by following: https://stackoverflow.com/a/36959225/842097
Don't use defaultValue, but set initialValues in the state
initialValues: {
name: theName
}
I have faced this bug as well, solved by mapping the props to a state first and then using that state instead with value prop.
function updateValue(e) {
this.setState({ value: e.target.value });
}
render {
return (
<TextField id="textfield_id" value={this.state.value} onChange={this.updateValue}/>
);
}

Categories