I am creating a multi step form using React JsonSchema Form. I want to update my state every time Next button is clicked on the page and finally Submit. React JsonSchema Form validates the entries only if the button is of type submit. So my Next button is submit button.
As the form will have multiple questions, my state is array of objects. Because of the asynchronous nature of useState, my updated state values are not readily available to save to the backend. How should I get final values?
When I debug I can see the data till previous step. Is there a way to make useState to behave like synchronous call?
Here is the full code:
const data = [
{
page: {
id: 1,
title: "First Page",
schema: {
title: "Todo 1",
type: "object",
required: ["title1"],
properties: {
title1: { type: "string", title: "Title", default: "A new task" },
done1: { type: "boolean", title: "Done?", default: false },
},
},
},
},
{
page: {
id: 1,
title: "Second Page",
schema: {
title: "Todo 2",
type: "object",
required: ["title2"],
properties: {
title2: { type: "string", title: "Title", default: "A new task" },
done2: { type: "boolean", title: "Done?", default: false },
},
},
},
},
];
interface IData {
id: Number;
data: any
};
export const Questions: React.FunctionComponent = (props: any) => {
const [currentPage, setCurrentPage] = useState(0);
const [surveyData, setSurveyData] = useState<IData[]>([]);
const handleNext = (e: any) => {
setSurveyData( previous => [
...previous,
{
id: currentPage,
data: e.formData,
},
]);
if (currentPage < data.length) setCurrentPage(currentPage + 1);
else //Here I want to submit the data
};
const handlePrev = () => {
setCurrentPage(currentPage - 1);
};
return (
<Form
schema={data[currentPage].page.schema as JSONSchema7}
onSubmit={handleNext}
>
<Button variant="contained" onClick={handlePrev}>
Prev
</Button>
<Button type="submit" variant="contained">
Next
</Button>
</Form>
);
};
You can incorporate useEffect hook which will trigger on your desired state change.
Something like this:
useEffect(() => {
// reversed conditional logic
if (currentPage > data.length) {
submit(surveyData);
}
}, [currentPage])
const handleNext = (e: any) => {
setSurveyData( previous => [
...previous,
{
id: currentPage,
data: e.formData,
},
]);
if (currentPage < data.length) setCurrentPage(currentPage + 1);
// remove else
};
On your last submit, you'll have all the previous data in surveyData, but you'll have the latest answer in e.formData. What you'll need to do is combine those and send that to the server.
// in your onSubmit handler
else {
myApiClient.send({ answers: [...surveyData, e.formData] })
}
I would refactor the new state structure to use the actual value of the state and not the callback value, since this will allow you to access the whole structure after setting:
const handleNext = (e: any) => {
const newSurveyData = [
...surveyData,
{
id: currentPage,
data: e.formData
}
];
setSurveryData(newSurveyData);
if (currentPage < data.length) {
setCurrentPage(currentPage + 1);
} else {
// submit newSurveryData
};
};
A side note: you'll also have to account for the fact that going back a page means you have to splice the new survey data by index rather than just appending it on the end each time.
Related
I have a state with the following structure. It contains a list of Workouts and each workout has a list of exercises related to this workout.
I want to be able to do 2 things:
add new exercises to the specific workout from the list of workouts
delete a specific exercise from the specific workout
E.g. In my UI I can add new exercises to Workout with the name Day 2.
So my action payload gets 2 params: workout index (so I can find it later in the state) and exercise that should be added to/deleted from the list of exercises of the specific workout.
State
state = {
workouts: [
{
name: "Day 1",
completed: false,
exercises: [{
name: "push-up",
completed: false
},
{
name: "running",
completed: false
}]
},
{
name: "Day 2",
completed: false,
exercises: [{
name: "push-up",
completed: false
}]
},
{
name: "Day 3",
completed: false,
exercises: [{
name: "running",
completed: false
}]
}]
}
Actions
export class AddExercise implements Action {
readonly type = ADD_EXERCISE
constructor(public payload: {index: number, exercise: Exercise}) {}
}
export class DeleteExercise implements Action {
readonly type = DELETE_EXERCISE
constructor(public payload: {index: number, exercise: Exercise}) {}
}
And I am stuck on the reducer. Can you advise how it should be done properly?
This is how it looks right now (not finalized yet):
Reducer
export function workoutsReducer(state = initialState, action: WorkoutActions.Actions) {
switch(action.type) {
case WorkoutActions.ADD_EXERCISE:
const workout = state.workouts[action.payload.index];
const updatedExercises = [
...workout.exercises,
action.payload.exercise
]
return {
...state,
workouts: [...state.workouts, ]
}
return {};
default:
return state;
}
}
Thank you!
Please, try something like the following (I included comments within the code, I hope it makes it clear):
export function workoutsReducer(state = initialState, action: WorkoutActions.Actions) {
switch(action.type) {
case WorkoutActions.ADD_EXERCISE:
// You can take advantage of the fact that array map receives
// the index as the second argument
// See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map
const workouts = state.workouts.map((workout, index) => {
if (index != action.payload.index) {
return workout;
}
// If it is the affected workout, add the new exercise
const exercises = [
...workout.exercises,
action.payload.exercise
]
return { ...workout, exercises }
})
// return the updated state
return {
...state,
workouts
}
case WorkoutActions.DELETE_EXERCISE:
// very similar to the previous use case
const workouts = state.workouts.map((workout, index) => {
if (index != action.payload.index) {
return workout;
}
// the new exercises array will be composed by every previous
// exercise except the provided one. I compared by name,
// I don't know if it is accurate. Please, modify it as you need to
const exercises = workout.exercises.filter((exercise) => exercise.name !== action.payload.exercise.name);
return { ...workout, exercises }
})
// return the new state
return {
...state,
workouts
}
default:
return state;
}
}
In my reducer, I have the initial state which looks like this:
const initialState = {
isLoading: false,
events: [
{
year: 2021,
place: [
{
id: 1,
name: "BD"
},
{
id: 2,
name: "BD Test"
}
]
},
{ year: 2020, place: [{ id: 3, name: "AMS" }, { id: 4, name: "AMS TEST" }] }
]
};
I have been trying to implement the functionality of deletion operation. So, when the button will be clicked the "deleteItems" action will be dispatched that will remove the corresponding items from the place. This functionality works fine. But,I am trying to remove the whole items from the events array if there is no values in place.
This is what I have tried already but it just removes the individual place. But, I need to write the logic here of removing the whole items when place becomes empty.
case "deleteItems":
return {
...state,
events: state.events.map(event => {
const place = event.place.find(x => x.id === action.id);
if (place) {
return {
...event,
place: event.place.filter(x => x.id !== action.id)
};
}
return event;
})
};
So, after modifications, the state would look like this:(when there is no values in place for year 2021)
const initialState = {
isLoading: false,
events: [
{ year: 2020, place: [{ id: 3, name: "AMS" }, { id: 4, name: "AMS TEST" }] }
]
};
Does anybody know how to accomplish this. Any helps would be highly appreciated.Thanks in Advance.
Demo can be seen from here
I removed the places first.
Then I filtered events based on whether the place array is empty or not.
After that, I returned the state.
case "deleteItems":
const eventsPostDeletingPlaces = state.events.map(event => {
const place = event.place.find(x => x.id === action.id);
if (place) {
return {
...event,
place: event.place.filter(x => x.id !== action.id)
};
}
return event;
});
const eventsWithPlaces = eventsPostDeletingPlaces.filter((each) => each.place.length);
return {
...state,
events: eventsWithPlaces
}
Check the edited sandbox here
Basically the same logic as in the first answer, but with reduce instead of a map and an extra filter. Just an option.
case "deleteItems":
return {
...state,
events: state.events.reduce((events, event) => {
const place = event.place.find(x => x.id === action.id);
if (place) {
event.place = event.place.filter(x => x.id !== action.id);
}
if (event.place.length > 0) {
events.push(event);
}
return events;
}, [])
};
codesandbox
I have setup my table in admin side of our application with MDBReact using the datatable. This table shows some small details of the stories that I have.
Now I have to make a row clickable i.e. add onClick to make a function call with the story id passed as an argument to this function.
Question:
How do I add onClick event to the datatable row?
(Below is my code.)
class Posts extends Component {
componentDidMount() {
this.getPosts();
}
getPosts = async () => {
const response = await fetch("http://****************/get_posts");
const post_items = await response.json();
this.setState({ posts: post_items.result }, () => {
console.log(this.state.posts);
this.setState({ tableRows: this.assemblePost() });
});
};
assemblePost = () => {
let posts = this.state.posts.map((post) => {
let mongoDate = post.dateAdded.toString();
let mainDate = JSON.stringify(new Date(mongoDate));
return {
postTitle: post.storyTitle,
// postDescription: post.storyDescription,
dateAdded: mainDate.slice(1, 11),
thankedBy: post.thankedBy.length,
reportedBy: post.reportedBy ? post.reportedBy.length : "",
userEmail: post.userEmail[0],
categoryName: post.categoryName[0],
};
});
console.log(posts);
return posts;
};
state = {
posts: [],
tableRows: [],
};
render() {
const data = {
columns: [
{
label: "Story Title",
field: "postTitle",
},
{ label: "Category Name", field: "categoryName" },
{
label: "User Email",
field: "userEmail",
},
{
label: "Date Added",
field: "dateAdded",
},
{
label: "Thanked",
field: "thankedBy",
},
{
label: "Reported",
field: "reportedBy",
},
],
rows: this.state.tableRows,
};
return (
<div className="MDBtable">
<p className="posts">Posts List</p>
<MDBDataTable striped bordered hover data={data} />
</div>
);
}
}
export default Posts;
To pull this off, here's what I did, but you'll need to appreciate these:
MDBDataTable requires you to manually define the columns and rows.
For data to render seamlessly, you define columns.field that correspond to rows[key]
Now, here's the logic, if you define a rows[key] that does not correspond to any columns.field, then that rows[key] is defined for an entire row just like we often pass index when working with map().
So based on the above observations,you can just pass the click event as a key/value pair to the row.And it will work just fine.
// ...
assemblePost = () => {
let posts = this.state.posts.map(
(post, i) => {
let mongoDate = post.dateAdded.toString();
let mainDate = JSON.stringify(new Date(mongoDate));
return {
index: i + 1, // advisable to pass a unique identifier per item/row
clickEvent: () => this.handleClick(storyId), // pass it a callback function
postTitle: post.storyTitle,
// ...others
categoryName: post.categoryName[0],
};
});
console.log(posts);
return posts;
};
// ...
Notice this clickEvent: () => this.handleClick(storyId), will be attached to the entire row.
I want to access state data.
How do I copy an array object I have into a data array?
-code
function TableList() {
const [state, setState] = React.useState({
columns: [
{ title: '로트번호', field: 'lote_id' },
{ title: '중량(kg)', field: 'item_weight' },
{ title: '수량', field: 'item_quantity' },
{ title: '제품코드', field: 'item_code' },
{ title: '제품명', field: 'item_name' },
{ title: '규격', field: 'standard' },
{ title: '재질', field: 'material' },
{ title: '공정명', field: 'process_name' },
{ title: '단중', field: 'unitweight' },
{ title: '납기일시', field: 'duedate' },
{ title: '단가', field: 'item_cost' },
{ title: '금액', field: 'item_price' },
],
data: [],
});
useEffect(() =>{
console.log("실행")
console.log(state.data);
axios.get('http://localhost:8080/api/item/1')
.then( response => {
//I want to copy the data of my output.list to state.data.
})
.catch( response => {
console.log(response);
})
});
I want to copy my axios response data into the data array declared as useState.
Please tell me the most effective way.
How do I access the second argument, state.data?
I want to access and put my output.list response data.
Not totally clear on what you're looking for, but if you just want to update the data prop on your state object, you can do it like this:
const arrayOfObjects = [{ id: 1 }, { id: 2 }];
const newState = Object.assign({}, state);
newState.data = arrayOfObjects;
setState(newState);
This is not a great way to use useState.
const [columns,setColumns] = React.useState([bla,bla]);
const [data, setData] = React.useState([bla, bla]);
You should declare your state variables separately like above.
useEffect(() =>{
console.log("실행")
console.log(state.data);
axios.get('http://localhost:8080/api/item/1')
.then( response => {
//console.log(response);
var output = response && response.data;
const newState = Object.assign({}, state);
newState.data = output.list;
setState(newState);
})
.catch( response => {
console.log(response);
})
});
I want to call it just once like the mounted function
From the step 1 dropdown if the user is picking one after submit redirect it to step2schema, and if he is picking two redirect him after submit to step3schema. Here is the jsfiddle (check my link):
const Form = JSONSchemaForm.default;
const step1schema = {
title: "Step 1",
type: "number",
enum: [1, 2],
enumNames: ["one", "two"]
};
const step2schema = {
title: "Step 2",
type: "object",
required: ["age"],
properties: {
age: {type: "integer"}
}
};
const step3schema = {
title: "Step 3",
type: "object",
required: ["number"],
properties: {
number: {type: "integer"}
}
};
class App extends React.Component {
constructor(props) {
super(props);
this.state = {step: 1, formData: {}};
}
onSubmit = ({formData}) => {
if (this.state.step === 1) {
this.setState({
step: 2,
formData: {
...this.state.formData,
...formData
},
});
} else {
alert("You submitted " + JSON.stringify(formData, null, 2));
}
}
render() {
return (
<Form
schema={this.state.step === 1 ? step1schema : step2schema}
onSubmit={this.onSubmit}
formData={this.state.formData}/>
);
}
}
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet"/>
<script src="https://npmcdn.com/react-jsonschema-form#1.0.0/dist/react-jsonschema-form.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
https://jsfiddle.net/bogdanul11/sn4bnw9h/29/
Here is the lib I used for documentation: https://github.com/mozilla-services/react-jsonschema-form
What I need to change in order to have my desired behavoir ?
Replace your state to this first
this.state = {step: 0, formData: {}};
Then replace your onSubmit logic to
onSubmit = ({formData}) => {
const submitted = formData;
this.setState({
step: submitted,
formData: {
...this.state.formData,
...formData
},
})
}
Finally replace your schema logic
schema={this.state.step === 0 ? step1schema : ( this.state.step === 1 ? step2schema : step3schema)}
the schema selecting ternary operation is what we call a compound ternary operation. If state.step is 0 , we select step1schema... if not a new selection statement ( this.state.step === 1 ? step2schema : step3schema) is run; in which if step is 1, step2schema is selected and for anything else step3schema.
Lets say you have more than 3 schemas and you want to access them. For that you will have to define the schemas in an array and access them using the index.
schemaArray = [ {
// schema1 here
} , {
// schema2 here
} , {
// schema3 here
}, {
// schema4 here
}, {
....
}]
Now your schema selecting you will have to use
schema = { schemaArray[ this.state.step ] }