I have created a dynamic form in React JS which adds new fields upon clicking the plus button. However when I try to update the form values using the onChange event it overwrite or sometimes deletes the old array. Why is this happening? What is the correct way to dynamically add values to an array?
Below is a screenshot of the error as shown in the console. The array adds the details of the initial person very well but after that when I try to enter "Person 1 details" the old array is somehow cleared. As soon as I enter contact number for Person 1 the array is again cleared.
My code can be found here: https://codesandbox.io/s/awesome-paper-kb7fgf?file=/src/Register.js
This is not the right approach you should not insert JSX elements into state. It is better to insert data in state and then derive UI from it.
The reason that could be causing your problem could be related to stale closures because when you store the JSX in state together with change handlers, the versions of change handlers that get stored belong to that render, hence they capture the formData with values from that render.
Anyway like I said you should discard that approach. I will show you below code how you should approach that problem but you will have to expand this to your use case, this is minimal use case. In this example you can add many persons and each person has input field for name property.
import React from 'react';
import './style.css';
import { v4 as uuidv4 } from 'uuid';
export default function App() {
let [persons, setPersons] = React.useState({
[uuidv4()]: {
name: 'john',
},
});
console.log(persons);
return (
<div>
<button
onClick={() => {
setPersons({
...persons,
[uuidv4()]: {
name: '',
},
});
}}
>
Add new person
</button>
{Object.entries(persons).map(([k, v]) => {
return (
<div key={k}>
<div>Person </div>
<input
value={v.name}
onChange={(e) => {
setPersons({
...persons,
[k]: {
...persons[k],
name: e.target.value,
},
});
}}
></input>
</div>
);
})}
</div>
);
}
Related
I'm having trouble understanding why a list won't update in React. For my website that I'm building, I'm trying to add a 'favorites' button, but when you click the button it updates the state but the changes never re-render in the list. I tried to make a simpler version, but this doesn't work either:
import React, { useState } from 'react';
import './App.css';
function App() {
const [favorites, setFavorites] = useState([]);
function addFavorite(name, id) {
let newFavorites = favorites;
let newFav = {name: name, id: id};
newFavorites.push(newFav);
setFavorites(newFavorites);
}
return (
<div className="App">
<ul>
{favorites.map((val) => {
return(<li key={val.id}><span>{val.name}</span></li>);
})}
</ul>
<button onClick={() => addFavorite("Thing1", 1)}>Thing 1</button>
<button onClick={() => addFavorite("Thing2", 2)}>Thing 2</button>
<button onClick={() => {console.log(favorites)}}>Check</button>
</div>
);
}
export default App;
I can see the state changes in the console when I log them, but the <ul> element never updates. I've looked online but most of the articles I've found have not been very helpful (I feel the example code I wrote looks a lot like this article.
let newFavorites = favorites;
This assigns newFavorites to point to favorites
newFavorites.push(newFav);
Because newFavorites points to favorites, which is an array in state, you can't push anything onto it and have that change render.
What you need to do, is populate a new array newFavorites with the content of favorites.
Try
const newFavorites = [...favorites];
That should work
I would make some changes in your addFavourite function:
function addFavorite(name, id) {
let newFav = {name, id};
setFavorites([…favourites, newFav]);
}
This way, everytime you click favourite, you ensure a new array is being created with spread operator
Its not working because use are mutating the existing state.
The list is updating but it won't render as useState only renders when the parameter passed to it is different from previous one but in your case though you are changing the list items still the reference is not altering.
To make it work you can use spread operator for lists for even Array.concat() returns a new updated array.
function addFavorite(name, id) {
let newFav = {name: name, id: id};
setFavorites(prev=>[...prev, newFav]);
}
For changing array state, you should use:
function addFavorite(name, id) {
let newFav = { name: name, id: id };
setFavorites((favorites) => [...favorites, newFav]);
}
I'm working on a next JS, React, Apollo, graphQL, faunaDB App. I am trying to architect how a form builder would work with it's mutations to fauna via graphQL. I am able to run mutations from the playground, and can query from the front end and build my form. Interaction seen here https://www.loom.com/share/7f7d1e1231d445f2be6b5db2c81239b6
Now I am pretty sure I can figure out how to run a mutation on the front end, my concern is when to run it? See I have the following code. Which queries faunaDB, and outputs form input elements from the state (which is populated by the query) (currently just ones of the type text) and allows you to add new form input types to the state causing a rerender and displaying a new form input element. This is all well and good.
import { useQuery, gql } from "#apollo/client";
import { useEffect, useState } from "react";
import { v4 as uuidv4 } from "uuid";
const INPUT_VALUES = gql`
query GetInputValues {
allFormInputVals {
data {
name
_id
type
}
}
}
`;
const Home = () => {
const { loading, error, data } = useQuery(INPUT_VALUES);
const [formState, setFormState] = useState(undefined);
useEffect(() => {
setFormState(data?.allFormInputVals?.data);
}, [data]);
const addInput = () => {
const blanktext = {
__typename: "FormInputType",
name: "Product Image",
_id: uuidv4(),
type: "text",
};
console.log(formState);
setFormState([...formState, { ...blanktext }]);
};
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<>
<form>
<input type="button" value="Add Form Input" onClick={addInput} />
{formState?.map((val, idx) => {
const nameId = `name-${idx}`;
const typeId = `type-${idx}`;
return (
<div key={val._id}>
{val.type === "text" && (
<>
<label htmlFor={nameId}>{`Name #${idx + 1}`}</label>
<input
type="text"
name={nameId}
id={nameId}
className={val.type}
/>
<label htmlFor={typeId}>{`Type #${idx + 1}`}</label>
<select name={typeId} id={typeId} className={val.type}>
{data.allFormInputVals.data.map((item) => {
return (
<option key={item._id} value={item.type}>
{item.type}
</option>
);
})}
</select>
</>
)}
</div>
);
})}
</form>
</>
);
};
export default Home;
However my mutation options in the playground seem to be limited to only adding one what is called a document at a time, which is one object. If I console log my state that I want to add to the db it looks as follows. Now I can do a mutation in the playground where I can add one of these objects. But I want to be able to add all of them at once. On a save operation. I want to do this because I dont want to run a request of every addition to the form, I want to use react state to handle the form until the very end and then do my db request.
[
{
"__typename":"FormInputVal",
"name":"name",
"_id":"291541872966369805",
"type":"text"
},
{
"__typename":"FormInputVal",
"name":"name",
"_id":"291541888089981453",
"type":"text"
},
{
"__typename":"FormInputVal",
"name":"Product Image",
"_id":"255f95e0-bff1-4e75-81fc-d6f3f9a72446",
"type":"text"
}
]
Now I have created a graphQL schema the has Users, which can have many Forms, and Forms which can have many inputs. It looks like so. The #relation directive is specifice to faunaDB. It works how I expect it to, beyond this mutation issue I have been mentioning.
type Form {
name: String!
index: Int!
user: User
formInputVals: [FormInputVal!] #relation
}
type FormInputVal {
name: String!
index: Int!
type: String!
formRoot: Form!
}
type User {
name: String!
email: String!
password: String!
forms: [Form] #relation
}
type Query {
allForms: [Form!]
allUsers: [User!]
allFormInputVals: [FormInputVal!]
}
See I can mutate the DB with the following. Where I select a specific form and add a input, thus causing a rerender of the frontend and the form input shows up. This is all well and great. This is an example muation of that type.
mutation{
createFormInputVal(data:
{formRoot:{connect:"291541554941657608"},name:"name",type:"text",index:0}){
name
type
index
formRoot{
name
}
}
}
But here is where the root of the problem lies.
I want to take that state created by react and add it to a faunaDB collection which is called formInputVal the graphql schema is mapped to the db collections.
I talked to Fauna support and they mentioned a #resolver directive where I can run a DB function and add multiple documents(objects) at once so far the lambda function syntax for faunaDB is above my understanding. They mentioned this article for the function https://docs.fauna.com/fauna/current/tutorials/ecommerce#function and this one for the resolver https://forums.fauna.com/t/placing-an-index-on-a-field-in-an-embedded-type/778/4
Let's clarify,
Am I approaching this right? I am open to changing the schema. What would you do if you wanted to solve this problem with alternative approches or the same approach, but with a missing piece I don't understand.
Why can't I just pass an array of objects to the mutation for the correct formID, and it add's that many document's to the collection in one query. Is there any sort of general pratice for creating a generative form like this.
Ok thanks any help ahead of time.
UPDATE:
I have tried the following mutation but it does not work. It haults with the following error Unknown argument 'formInputVal' on field 'createFormInputVal' of type 'Mutation'
const ADD_INPUT_VALUES = gql`
mutation AddInputValues($formInputVal: FormInputValInput!) {
createFormInputVal(formInputVal: $formInputVal) {
name
}
}
`;
const [createFormInputVal, { data: createInputData }] = useMutation(
ADD_INPUT_VALUES
);
...
<form
onSubmit={async (e) => {
e.preventDefault();
const res = await createFormInputVal({
variables: formState,
}).catch(console.error);
console.log(res);
}}
>
...
If the root of the issue here is mutating or creating multiple documents at once, I think it got duplicated here:
https://stackoverflow.com/a/68930202/534056
A couple of things to watch out for:
Choose a name for your custom inputs that does not equal [type name] + Input. This is what Fauna uses for the auto-generated CRUD operations, and if you define one yourself the it will override the generated one. For example, given the type Form, Fauna will generate an input type called FormInput, so don't use that for a custom one.
If you want to pass an array of data as input, specify the argument as a List type (placed in square brackets).
If you are updating some documents, you will need to pass in the ID. The GraphQL API hides some details around Refs, so in your UDF, you will need to reconstruct the ref from an ID.
I have a simple react component which takes in a prop which is an Object.
I am trying to pass this in to a dispatch call to get it added to a list.
This works fine if I pass in a static value for the description field.
But fails when I use the dynamic field. I have checked via console log and the dynamic field definitely has a value and it is of type String as expected.
Why does it work when I use a static value (done solely for testing) for now but fails when I use the dynamic value?
Don't see any issues with my store, action, reducer setup cos it updates state fine with a static value.
Please advice. Thank you.
Experiencing this issue inside the ExpenseForm component existing within AddExpensePage where I
am using the expense value being passed in to perform the dispatch.
This is the working example with the static String value.
import React from "react";
import ExpenseForm from "./ExpenseForm";
import {connect} from "react-redux";
const AddExpensePage = (props) =>
<div>
<h1>Add Expense</h1>
<ExpenseForm onSubmit={(expense) => {
// This is what I want to do but unable to cos of the issue mentioned with description value thus breaking it out as follows.
// props.dispatch(addExpense(expense))
console.log(expense.description) // I do get a value like 'sample input' thus no issue with value
console.log(typeof expense.description) // it is of type String as expected
// breaking out the expense to be able to hard code value for description.
const tempFakeExpense = {
description: 'HARD_CODED_STRING_WHICH_WORKS', // expense.description will not update
note: expense.note,
amount: expense.amount,
createdAt: expense.createdAt
}
props.dispatch(addExpense(tempFakeExpense))
props.history.push('/');
}}/>
</div>
export default connect()(AddExpensePage);
The above doesn't work if I use the dynamic value as follows.
Meaning, if list had 10 items, it remains as 10. It doesn't append list to make it 11.
const actualExpense = {
description: expense.description, // not using hard coded value, thus not updating. Why?
note: expense.note,
amount: expense.amount,
createdAt: expense.createdAt
}
Expecting the issue to exist in above code. Adding the Expense component code below for reference in case there is some form of red herring.
There is no Redux in this component cos the state values are only for input tracking and not used anywhere else. Thus sticking to plain state/ setstate style of state management.
Note: The value got passed correctly to above component thus this state management works fine for ExpenseForm component and it doesn't need Redux to work in this component.
import React from 'react';
import moment from "moment";
import {SingleDatePicker} from "react-dates";
import 'react-dates/lib/css/_datepicker.css';
class ExpenseForm extends React.Component {
state = {
description: '',
note: '',
amount: '',
createdAt: moment(),
focused: false
}
render() {
return (
<div>
<form onSubmit={(e) => {
e.preventDefault();
if(this.state.description && this.state.amount) {
this.props.onSubmit({
description: this.state.description,
amount: parseFloat(this.state.amount),
note: this.state.note,
createdAt: this.state.createdAt.valueOf()
})
}
}}>
<input
type='text'
placeholder='description...'
autoFocus
value={this.state.description}
onChange={e => {
this.setState(() => ({
description: e.target.value
}))
}}
/>
{/* Setups for other fields */}
<button>Add Expense</button>
</form>
</div>
);
}
}
export default ExpenseForm;
I'm trying to create an input text inside a react component and then I realised that it's a bad praxis. So I investigated a little bit so I found Controlled-Components, so I think this is what I need, but looking at my Component I do not know how to create it.
I do not have an extends Redux.Component so a friend suggested me to create a Component but couldn't get succeed.
What I was trying is this :
Inside my component
<input
...
/>
{" "}
<input
...
/>
<span>
<myButton
...
arguments={[document.getElementById("id1").value, document.getElementById("id2").value]}
>
[ send ]
</myButton>{" "}
</span>
But I'm getting this error :
The given id must not be null!; nested exception is java.lang.IllegalArgumentException: The given id must not be null!
EDIT
On my component where I have all of those code I have this :
<myButton
id={id}
arguments={[intputStuff]}
>
So my problem is if I do what Tom's says I do not have the id in the other component.
So the thing should be create this component inside the other component and then get the values of the inputtexts and put them as an arguments
It's not clear from your post what exactly you're trying to accomplish.
It appears that you're trying to build a component with 2 text inputs and a button.
If you want the button to "submit" the values of the two inputs, you should do something like this:
class SomeComponent extends React.Component {
constructor(props) {
super(props)
this.state = {
value1: props.initialValue1,
value2: props.initialValue2
}
}
onChangeText = (event) => this.setState({ [event.target.name]: event.target.value })
onClickSubmit = (event) => {
let { onSubmit } = this.props
if(typeof onSubmit !== 'function') return
let { value1, value2 } = this.state
return onSubmit([ value1, value2 ])
}
render() {
let {
initialValue1,
initialValue2,
onSubmit,
className,
...props
} = this.props
let {
value1,
value2
} = this.state
return (
<div className={`SomeComponent ${className}`} {...props}>
<input value={value1} name="value1" onChange={this.onChangeText} />
<input value={value2} name="value2" onChange={this.onChangeText} />
<button onClick={this.onClickSubmit}>
Submit
</button>
</div>
)
}
}
A few notes:
This example uses a bunch of futuristic JS: destructuring, rest/spread, class properties, computed property names, and arrow functions. Each feature is being leveraged for a specific purpose, not just because they're cool. If your environment doesn't support some of these features, you'll need to find a workaround that makes good on some additional constraints.
This is not a controlled component, but it does contain 2 controlled inputs. It uses the "initialValue" pattern: the owning component provides starting values, but is unaware of the blow-by-blow as the user types each character. The owning component is only notified of the new values when the button is clicked. This pattern can result in loss of data if the owner is re-rendered before the current value are submitted.
Generally, when using React, you want to avoid using native DOM methods to access or manipulate elements. (There are plenty of exceptions, of course.) One reason you want to avoid native DOM methods is that component lifecycle methods might execute before the React renderer has actually updated the DOM -- so document.getElementById('someid') might return undefined.
I have an array with 3 elements with each element of the array having 2 objects and looking like this:
text{field1: 'xyz', field2: 'def'}
metadata{id: 42, timestamp: '02-25-2018'}
On a react app, I have a list of elements with each one displaying array[i].text.field1 . I would like to implement a click handler where when the user clicks an element, it switches a piece of the state (based on the element they clicked). How do I index into the right element in my array based on the nested field1 value? Let me know if I need to add more color here.
Create Element stateless component and pass element data from array as props. Using closure pass this props to onClick handler.
import React from 'react';
export default (props) => {
const { text: { field1, field2 } } = props;
return <div>
<h1
onClick={((e) => onFieldClick(e, props))}
>
{field1}
</h1>
</div>;
};
const onFieldClick = (e, props) => {
alert(props.text.field1);
}
Demo