Components Will Not Render After a UseState Change - javascript

This is a difficult question because there are so many moving parts, but allow me to attempt to explain the scenario before I start shoving code in everyone's face.
My goal is to allow managers to have a screen where all their drivers are displayed. They will have minimal information displayed and an edit button. If the user clicks the edit button they will stay on the same page. There is a useState, const [driverSelected, setDriverSelected] = useState("") that once an edit button is clicked, will call setDriverSelected to be the driver, not just the id. So once an edit button is clicked, an actual new value for driverSelected would look like this...
{id: 'a049c673-da36-48e6-8fbd-32ab925b6178', role: 'USER', firstname: 'STEVEN', lastname: 'MONROE', email: 'TQRGJGNFQVIO', …}
deleted: false
email: "TQRGJGNFQVIO"
firstname: "STEVEN"
id: "a049c673-da36-48e6-8fbd-32ab925b6178"
lastname: "MONROE"
locked: false
phoneNumber: "null"
profilePick: null
role: "USER"
__typename: "Driver"
[[Prototype]]: Object
Based on this, the same page will change from displaying all the drivers to just the one selected, and input fields to change his/her attributes. This all works properly.
From here, you hit submit and it sends a mutation over to the database. This also works. Then, a query is automatically launched to send the user back the new driver data. This also also works. Where everything breaks is once the mutations/queries are run, I also run setDriverSelected({id: -1}) which should render the drivers list again, but nothing appears at all.
I thought it may be an issue with the data flow, but it isn't. I have console.log statements everywhere along the way from the mutation to the re-render, and at every point the console.log statements return exactly what they're supposed to. No errors in the console, no failed fetches or anything like that from the network. I just literally get nothing. I've even tried replacing all the data with static information, still nothing.
The code is all spread out too across about 7 files since I was trying to compartmentalize as much as possible while using React, so bare with the ugly mess of code files you're about to see.
This is the first page in question, the one that is in charge of either rendering the list OR the driver's fields when chosen.
import React from "react";
import { useState } from "react";
import { useRecoilState } from "recoil";
import { userState } from "../../recoil/atoms";
import SideMenu from "../../components/Home/SideMenu/SideMenu";
import DriverCard from "./DriverCard";
import EditDriver from "./EditDriver";
import "../../styles/EditDrivers/EditDriversLanding.css"
const EditDriversLanding = () => {
// Recoil Data
const rawUser = useRecoilState(userState)
console.log(rawUser)
const user = rawUser[0]
// Local states
const [getSearch, setSearch] = useState("")
const [driverSelected, setDriverSelected] = useState({id: -1})
// Based off of what you type in the search bar, it will filter out invalid employees
const filterDriversList = (list) => {
let filteredList = []
if (getSearch == ""){
return list
}
else{
let filterString = getSearch.toUpperCase()
list.forEach( (driver) => {
if (driver.firstname.includes(filterString) || driver.lastname.includes(filterString)){
filteredList.push(driver)
}
})
return filteredList
}
}
// Takes the list of drivers and renders them all into a list of components
const renderDriverCards = (list) => {
let i = 0
console.log("Okay.... like dude you're RIGHT here, RENDER")
console.log(list)
return list.map( (driver)=> {
i++
if (i == 1){
console.log(driver)
console.log("WHY WONT YOU WORK???")
}
return (<DriverCard driver={driver} key={i} setDriverSelected={setDriverSelected} />)
})
}
const renderListOrEditScreen = () => {
// No Driver selected
if (driverSelected.id == -1){
console.log("dude.... render!!!")
return(
<div className="overlay">
<div className="edit-landing-container">
<SideMenu />
<div className="edit-landing-search-bar">
<input type="text" onChange={(event) => setSearch(event.target.value)} />
</div>
<div className="edit-landing-drivers-list">
{renderDriverCards(filterDriversList(user.drivers))}
</div>
</div>
</div>
)
}
// Driver Selected
else{
return(
<div className="overlay">
<div className="edit-landing-container">
<SideMenu />
<div>
<EditDriver driverData={driverSelected} setDriverSelected={setDriverSelected}/>
</div>
</div>
</div>
)
}
}
if (driverSelected.id == -1){
console.log("should be rendering...")
}
return (
<div>
{renderListOrEditScreen()}
</div>
)
}
export default EditDriversLanding
Its worth mentioning again that this file above works perfectly the first time it is rendered, but after a driver is edited, NOTHING renders-- not a single <div>
Here is the file for the <DriverCard />
import React from "react";
import "../../styles/EditDrivers/EditDriversLanding.css"
const DriverCard = ({driver, setDriverSelected}) => {
console.log(driver)
console.log("dude just work bro")
return(
<div className="edit-drivers-driver-card">
<div>
Image
</div>
<div>
<div>{driver.firstname} {driver.lastname}</div>
<div>{driver.email}</div>
<div>{driver.phoneNumber}</div>
<div className="edit-driver-driver-card-edit-button" onClick={() =>setDriverSelected(driver)}>Edit</div>
</div>
</div>
)
}
export default DriverCard
And finally, here is the EditDriver page which is where the mutation and re-query take place. Notice here you'll see a <div> that on press will also setDriverSelected({id: -1}) and THAT one decides to work-- just the submitting changes kills everything.
import React from "react";
import { useState } from "react";
import DriverField from "../../components/EditDrivers/DriverField";
import SubmitEdits from "./submitEdits";
import "../../styles/EditDrivers/EditDriversLanding.css"
const EditDriver = ({driverData, setDriverSelected}) => {
const [driver, setDriver] = useState(driverData)
const handleInput = (event) => {
const input = { ...driver };
input[event.target.id] = event.target.value;
setDriver(input);
};
return(
<div className="edit-driver-editting-page">
<div onClick={() => setDriverSelected({id: -1})} className="edit-driver-editting-page-exit-button">
Return to Driver Selection
</div>
<div>
<h2>Edit {driverData.firstname} {driverData.lastname}</h2>
</div>
<div>
<div>
<DriverField currentValue={driver.firstname} name="firstname" handleInput={handleInput} />
</div>
<div>
<DriverField currentValue={driver.lastname} name="lastname" handleInput={handleInput} />
</div>
<div>
<DriverField currentValue={driver.email} name="email" handleInput={handleInput} />
</div>
<div>
<DriverField currentValue={driver.phoneNumber} name="phoneNumber" handleInput={handleInput} />
</div>
<SubmitEdits driver={driver} setDriverSelected={setDriverSelected}/>
</div>
</div>
)
}
export default EditDriver

It's hard to say why nothing at all is rendering - but it looks like your landing page component is more complex than it needs to be. It's not often that you need to have functions which render content (e.g. renderListOrEditScreen and renderDriverCards) - often that's a sign that you should break those functions out into their own components.
So, I'd suggest you start by splitting that up into smaller components that do less work. It looks like one of the functions of that page is to act as the "search" page - you could split that up using something like this:
const useFilteredDriversList = (drivers, search) => {
return useMemo(() => {
if (!search) return drivers;
const searchUpper = search.toUpperCase();
return drivers.filter(driver =>
driver.firstName.includes(searchUpper) ||
driver.lastName.includes(searchUpper)
);
}, [drivers, search]);
}
const DriverSearch = ({ onDriverSelected }) => {
const [user] = useRecoilState(userState);
const [search, setSearch] = useState("");
const filteredDrivers = useFilteredDriversList(user.drivers, search);
const handleSearchChange = (event) => setSearch(event.target.value);
return (
<>
<div className="edit-landing-search-bar">
<input type="text" onChange={handleSearchChange} />
</div>
<div className="edit-landing-drivers-list">
{filteredDrivers.map(driver => (
<DriverCard
key={driver.id}
driver={driver}
setDriverSelected={onDriverSelected}
/>
))}
</div>
</>
);
}
Note here I've also split out the filtering code from the component - having it inside the component means you're redefining the filter function every time the component renders, which is unnecessary.
OK; now that the search page has been split out, you can just have a landing page component which either shows the search component or the edit component, depending on if a driver has been selected or not. One other thing that I'd do is create an explicit handler for the case of "cancelling" the edit, and have that live in the landing page. The edit page shouldn't have knowledge of how to "cancel" editing (i.e. setting the driver to { id: -1 }) - that's not its responsibility. It should just tell the parent component that it's finished, and let the parent component worry about how to handle that.
Finally, I'd use either null or undefined to represent "no driver selected" rather than a magic object. So, something like this might work:
const DriversPage = () => {
const [driver, setDriver] = useState(undefined);
const handleUnselectDriver = () => setDriver(undefined);
return (
<div className="overlay">
<div className="edit-landing-container">
<SideMenu />
{driver && (
<EditDriver
driverData={driver}
onEditComplete={handleUnselectDriver}
/>
)}
{!driver && (
<DriverSearch onDriverSelected={setDriver} />
)}
</div>
</div>
);
}

Related

HTML changes are not saved when a checkbox is set and the page is changed in React + Bootstrap

I have an array with several "question" elements, each of them with a structure similar to this:
<><div className="row"><div className="col-12 col-md-9 col-lg-10 col-xl-10 col-xxl-10"><span>Question 1</span></div><div className="col-3 col-md-1 col-lg-1 col-xl-1"><div className="form-check"><input id="formCheck-1" className="form-check-input" type="checkbox" /><label className="form-check-label" for="formCheck-1">Yes</label></div></div><div className="col-3 col-md-1 col-lg-1 col-xl-1"><div className="form-check"><input id="formCheck-2" className="form-check-input" type="checkbox" /><label className="form-check-label" for="formCheck-2">No</label></div></div></div></>,
In order to give each element a bit of keyed structure, I store each array element in a helper component. The HTML of the questions is simply stored in "element". Simple:
const ElementoPaginacion = ({element}) =>{
return(
element
)
}
Since there can be many of these elements in this array, they are displayed with pagination. The displayed page is calculated (apparently correctly, using a simple calculation). The code snippet that calculates and displays it is as follows:
<>
{
//Calculate init index (it depends ont the current page) to show the questions, and the number of elements to show (its rangePages)
fullList.slice(currentPage * rangePages, (currentPage * rangePages) + rangePages).map((current) => (
<React.Fragment key={current.key}>
{current}
</React.Fragment>
))
}
What happens is that, when a change is made to that HTML by the user (for example, checking a checkbox), when changing the page, that change is NOT saved, if it is redrawed (for example, changing the page and returning to the same page afterwards). I am attaching images to see how it works:
We can see how we make changes to the questions on page 0, we change to page 1, and when we return to page 0 again, the changes (check ckeckboxes) have not been saved.
That could be happening?
------------- EDIT -------------
Okay. Right now, the idea is to save changes to any portion of HTML that is passed to us, be it a question with checkbox responses, or a radiobutton.
The problem: if we don't know what content is going to pass, what we have to save is all the content in html. Now, how can we save any HTML content that has been modified? I've tried creating this helper component, which wraps the HTML content passed to it inside a "div", but when clicked, how can I retrieve the new HTML content to reassign (ie the "newData" parameter)?
const ElementoPaginacion = ({element}) =>{
const [content, saveElement] = useState(element);
const saveData = (newData) =>{
saveElement(newData);
}
return(
<div onChange={saveData}>
{element}
</div>
)
}
Are you using a backend database? If not, the changes are stored in memory and will be lost each time you change the page.
You can use the useState hook to prevent the state of the forms from being lost each time the element is dismounted. Then the user can submit the state in its entirety after answering the questions.
Example:
import { useState } from 'react';
import { Button } from '#mui/material';
import { Checkbox } from '#mui/material';
function App() {
const [checkBoxIsMounted, setCheckBoxIsMounted] = useState(false);
const handleClick = () => {
setCheckBoxIsMounted(!checkBoxIsMounted)
}
return (
<>
<Button onClick={handleClick}>Mount Checkbox</Button>
{checkBoxIsMounted && <Checkbox />}
</>
);
}
export default App;
Note, I'm using MUI instead of Bootstrap, but the underlying principle is the same.
The above code snippet produces the following behavior:
If we add state to the checkbox, React will maintain the state in memory even after the component is dismounted:
import { useState } from 'react';
import { Button } from '#mui/material';
import { Checkbox } from '#mui/material';
function App() {
const [checkBoxIsMounted, setCheckBoxIsMounted] = useState(false);
const [checked, setChecked] = useState(false);
const handleClick = () => {
setCheckBoxIsMounted(!checkBoxIsMounted)
}
const handleChange = () => {
setChecked(!checked)
}
return (
<>
<Button onClick={handleClick}>Mount Checkbox</Button>
{checkBoxIsMounted && <Checkbox onChange={handleChange} checked={checked} />}
</>
);
}
export default App;
This modified snippet produces this behavior:

ReactJS prop is not a function - do I need a class component instead of function and call super()?

I'm following this tutorial on YouTube https://youtu.be/b9eMGE7QtTk
The full code can be found here: https://gist.github.com/adrianhajdin/997a8cdf94234e889fa47be89a4759f1
The tutorial was great, but it didn't split all the functionalities into components which is React used for (or I'm so lead to believe).
So we have the App.js
import React, { useState, useEffect } from "react";
import MovieCard from "./MovieCard";
import SearchIcon from "./search.svg";
import "./App.css";
const API_URL = "http://www.omdbapi.com?apikey=b6003d8a";
const App = () => {
const [searchTerm, setSearchTerm] = useState("");
const [movies, setMovies] = useState([]);
useEffect(() => {
searchMovies("Batman");
}, []);
const searchMovies = async (title) => {
const response = await fetch(`${API_URL}&s=${title}`);
const data = await response.json();
setMovies(data.Search);
};
return (
<div className="app">
<h1>MovieLand</h1>
<div className="search">
<input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search for movies"
/>
<img
src={SearchIcon}
alt="search"
onClick={() => searchMovies(searchTerm)}
/>
</div>
{movies?.length > 0 ? (
<div className="container">
{movies.map((movie) => (
<MovieCard movie={movie} />
))}
</div>
) : (
<div className="empty">
<h2>No movies found</h2>
</div>
)}
</div>
);
};
export default App;
MovieCards.jsx is as follows:
import React from 'react';
const MovieCard = ({ movie: { imdbID, Year, Poster, Title, Type } }) => {
return (
<div className="movie" key={imdbID}>
<div>
<p>{Year}</p>
</div>
<div>
<img src={Poster !== "N/A" ? Poster : "https://via.placeholder.com/400"} alt={Title} />
</div>
<div>
<span>{Type}</span>
<h3>{Title}</h3>
</div>
</div>
);
}
export default MovieCard;
The app works, but I want to move className="search" to be its own component like Search /.
The code I end up having in App.js is
//at the top of App.jx
import Search from "./Search"
// in const App
<Search prop={searchMovies}/>
And in the new Seach / component
import { useState } from "react";
import SearchIcon from './search.svg';
const Search = ( prop ) => {
const [searchTerm, setSearchTerm] = useState("");
return (
<div className="search">
<input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search"
/>
<img
src={SearchIcon}
alt="search"
onClick={() => prop(searchTerm)}
//props used to be searchMovies
/>
</div>
)
}
export default Search;
When typing something in the search field on the app and clicking on the search icon I get the following error:
prop is not a function
If my research has been correct, I need to use a constructor and super()
But it seems like the constructor needs to be called in a class Search instead of const Search as it breaks the code. Is that the case or is there a way to use the constructor in a function component, or is there something else completely that I should do?
Also, if there is a great tutorial you could recommend for super() I'd be really grateful.
Other thing that I want to do is to make a Results component or call it whatever that would have the {movies?.length > 0 ? ( part of the code, but I feel like that will be a different headache.
Basically what I want is to have:
const App = () => {
return (
<div className="app">
<h1>Movie Site</h1>
<Search />
<Results />
</div>
);
};
Or as shown in the picture
Hope all this makes sense. Also, I want to preface that I do not expect anyone to write the code for me, but if it helps me understand this it's appreciated. YT tutorials are appreciated as well.
Okay, after a push in the right direction from jonrsharpe and renaming the props into random things I figured it out.
As jonrsharpe said, my function is prop.prop, so if I wanted to call searchTerm in
onClick={() => prop(searchTerm)}
it should be
onClick={() => prop.prop(searchTerm)}
Now, that works, but looks silly. So renaming the first "prop" in prop.prop and the prop in const Search to searchOnClick leaves searchOnClick.prop(searchTerm) which still works. Great.
Then in App.js renaming prop in Search prop={searchMovies} to searchOnClick={searchMovies} needs to be followed by renaming searchOnClick.prop in Search.jsx to searchOnClick.searchOnClick.
Lastly, we want to destructure the props as jonrsharpe said.
const Search = ( searchOnClick ) => {
would become
const Search = ( {searchOnClick} ) => {
That allows us to remake searchOnClick.searchOnClick(searchTerm) to searchOnClick(searchTerm) only.
The whole point is that the prop calls the whole componentName variable=value but it doesn't take the value of the variable automatically so it needs to be called like prop.variable until destructured where it can be called as variable only.
Now that I figured this out it feels silly spending two days on this. Thanks to jonrsharpe again, and hope this helps to someone else in the future.

Search bar stopping props from changing

On my site, the <ArticleList> is supposed to update when one navigates between columns. This works when you go from the home page to a column, or from an article to a column. But if you go from column to column, it doesn't work. The page doesn't update at all, but the url changes. The links to each column stay the same, as they are part of the <Layout> component, which every page has.
Edit
I figured out now that I can just use <a> and omit <Link> entirely, but this would slow down the page navigation.
Edit 2
This is part of my <Layout> component where I render the links to the columns:
<nav className={layout.columnContainer}>
{columns.map(({ id, name }) =>
this.props.currentColumn ? (
<a key={id} href={`/columns/${name}`}>
{name}
</a>
) : (
<Link key={id} href="/columns/[name]" as={`/columns/${name}`}>
<a>{name}</a>
</Link>
),
)}
</nav>
Edit 3
My minimal reproducible example is on GitHub, and I get the same unexpected results.
Edit 4
I found that the reason it wasn't working was I implemented a search bar that put the children prop in a state and modified this.
Constructor:
constructor(props) {
super(props);
this.searchArticlesKeyType = this.searchArticlesKeyType.bind(this);
this.state = {displayedMain: props.children};
}
Inside the render method are the column links (nav) and the problematic search input element.
<nav className={layout.columnContainer}>
{
columns.map(({id, name}) => (
<Link key={id} href="/columns/[name]" as={`/columns/${name}`}><a>{name}</a></Link>
))
}
</nav>
<div className={layout.search}>
<input type="search" name="q" onKeyUp={this.searchArticlesKeyType} />
</div>
async searchArticlesKeyType(e) {
// Some code
this.setState({
displayedMain: <ArticleList articles={JSON.stringify(filteredArticles)}/>
// More code
});
}
I think your main issue here is the way you're implementing the search feature, you don't want to store components in state instead you need to pass the search text to the articlelist component and do the filtering there.
There are several ways to implement communication between 2 unrelated components, it could be via context, redux, or even make a portal in the layout to render the seach input from the column component, but in this case I think the best option is to store the search text in the url:
First make the input event update the url using next/router, your layout will look like this:
import { useRouter } from 'next/router'
...
function Layout(props) {
const {columns} = props;
const { push, asPath, query } = useRouter()
const searchArticlesKeyType = (e) => {
const q = e.target.value;
const [url] = asPath.split('?');
push(`${url}?q=${q}`, undefined, { shallow: true });
}
return (
<div>
...
<div>
<input type="search" name="q" defaultValue={query.q} onKeyUp={searchArticlesKeyType} />
</div>
...
</div>
)
}
And then you do the filtering in articlelist component
import Link from "next/link";
import { useRouter } from 'next/router'
export default function ArticleList(props) {
const { query } = useRouter();
const q = query.q || "";
const filteredArticles = props.articles.filter(
(item) => item.title.includes(q) || item.body.includes(q)
);
return (
<ul className="grid">
{filteredArticles.map((item) => (
<div key={item.id}>
<Link
key={item.id}
href="/articles/[title]"
as={`/articles/${item.title}`}
>
<a>
<p>
<strong>{item.title}</strong>
</p>
<p>{item.body.substring(0, 100)}</p>
</a>
</Link>
</div>
))}
</ul>
);
}

Send selected options from react dual listbox with post request

I'm trying to implement in my react app, two react double listbox in my component. At the moment the listboxes are filled automatically after a get request when component mounts. I need some help on how to get the selected options in each double listbox and send them to the server as json data.
I need two arrays from these lists.
This is my dual listbox classes:
import React from 'react';
import DualListBox from 'react-dual-listbox';
import 'react-dual-listbox/lib/react-dual-listbox.css';
import 'font-awesome/css/font-awesome.min.css';
export class FirstList extends React.Component {
state = {
selected: [],
};
onChange = (selected) => {
this.setState({ selected });
};
render() {
const { selected } = this.state;
return (
<DualListBox
canFilter
filterPlaceholder={this.props.placeholder || 'Search From List 1...'}
options={this.props.options}
selected={selected}
onChange={this.onChange}
/>
);
}
}
export class SecondList extends React.Component {
state = {
selected: [],
};
onChange = (selected) => {
this.setState({ selected });
};
render() {
const { selected } = this.state;
return (
<DualListBox
canFilter
filterPlaceholder={this.props.placeholder || 'Search From List 2...'}
options={this.props.options}
selected={selected}
onChange={this.onChange}
/>
);
}
}
In my component I started importing this:
import React, { useState, useEffect } from 'react'
import LoadingSpinner from '../shared/ui-elements/LoadingSpinner';
import ErrorModal from '../shared/ui-elements/ErrorModal';
import { FirstList, SecondList } from '../shared/formElements/DualListBox';
import { useHttpClient } from '../shared/hooks/http-hook';
const MyComponent = () => {
const { isLoading, error, sendRequest, clearError } = useHttpClient();
const [loadedRecords, setLoadedRecords] = useState();
useEffect(() => {
const fetchRecords = async () => {
try {
const responseData = await sendRequest(
process.env.REACT_APP_BACKEND_URL + '/components/get'
);
setLoadedRecords(responseData)
} catch (err) { }
};
fetchRecords();
}, [sendRequest]);
...
...
return (
<React.Fragment>
<ErrorModal error={error} onClear={clearError} />
<form>
<div className="container">
<div className="row">
<div className="col-md-6">
<fieldset name="SerialField" className="border p-4">
<legend className="scheduler-border"></legend>
<div className="container">
<p>SERIALS</p>
{loadedRecords ? (
<FirstList id='Serials' options={loadedRecords.firstRecordsList} />
) : (
<div>
<label>List is loading, please wait...</label>
{isLoading && <LoadingSpinner />}
</div>
)}
</div>
</fieldset>
</div>
<div className="col-md-6">
<fieldset name="SystemsField" className="border p-4">
<legend className="scheduler-border"></legend>
<div className="container">
<p>SYSTEMS</p>
{loadedRecords ? (
<SecondList options={loadedRecords.secondRecordsList} />
) : (
<div>
<label>List is loading, please wait...</label>
{isLoading && <LoadingSpinner />}
</div>
)}
</div>
</fieldset>
</div>
...
...
If anyone could guide me it'll be much appreciated.
Thanks in advance!
FirstList and SecondList are using internal state to show the selected values. Since a parent component should do the server request, it needs access to this data. This can be achieved by a variety of options:
Let the parent component (MyComponent) handle the state completely. FirstList and SecondList would need two props: One for the currently selected values and another for the onChange event. MyComponent needs to manage that state. For example:
const MyComponent = () => {
const [firstListSelected, setFirstListSelected] = useState();
const [secondListSelected, setSecondListSelected] = useState();
...
return (
...
<FirstList options={...} selected={firstListSelected} onChange={setFirstListSelected} />
...
<SecondList options={...} selected={secondListSelected} onChange={setSecondListSelected} />
...
)
Provide only the onChange event and keep track of it. This would be very similar to the first approach, but the lists would keep managing their state internally and only notify the parent when a change happens through onChange. I usually don't use that approach since it feels like I'm managing the state of something twice and I also need to know the initial state of the two *List components to make sure I am always synchronized properly.
Use a ref, call an imperative handle when needed from the parent. I wouldn't recommend this as it's usually not done like this and it's getting harder to share the state somewhere else than inside of the then heavily coupled components.
Use an external, shared state like Redux or Unstated. With global state, the current state can be reused anywhere in the Application and it might even exist when the user clicks away / unmounts MyComponent. Additional server requests wouldn't be necessary if the user navigated away and came back to the component. Anyways, using an external global state needs additional setup and usually feels "too much" and like a very high-end solution that is probably not necessary in this specific case.
By using option 1 or 2 there is a notification for the parent component when something changed. On every change a server request could be sent (might even be debounced). Or there could be a Submit button which has a callback that sends the saved state to the server.

Why I cant share info between form input (child component) and React useState Hook (parent component)?

I am doing a multiple step form with React and I expect that when the user hits "Next" button, he/she can go to the next form, save info in parent component useState Hook and keep it in the input if user decides to go back. Switch has 8 cases but I left only 1 for simplicity. Structure looks something like this (Questions after code):
<MainForm />
|_ <UserInfo />
|_ Another 7 childs
MainForm.js
import React, { useState } from "react";
import UserInfo from "../components/Shared/UserInfo";
//import ... more childs ...
import {
BackButton,
NextButton,
SendButton,
} from "../components/Shared/Buttons";
const BDMForm = (props) => {
const [step, setStep] = useState(1);
const [datos, setDatos] = useState({
fullName: "",
email: "",
phoneNumber: "",
linkedIn: "",
city: "",
experience: "",
});
const handleChange = (e) => {
setDatos({ ...datos, [e.target.name]: e.target.value });
};
function renderComponent(step) {
let currentStep = step;
switch (currentStep) {
case 1:
return <UserInfo handleChange={handleChange} datos={datos.fullName} />;
// 7 more cases
}
return (
<form>
{renderComponent(step)}
<div className="button-container">
<BackButton step={step} setStep={setStep} />
{step >= 7 ? null : <NextButton step={step} setStep={setStep} />}
{step === 7 ? <SendButton /> : null}
</div>
</form>
);
};
export default MainForm;
UserInfo.js
import React from "react";
return (
<div className="card-container">
<label>
Nombre y Apellido<span> *</span>
</label>
<input
type="text"
name="fullName"
value={props.datos}
onChange={props.handleChange}
/>
</div>
</div>
);
};
export default UserInfo;
I have 2 questions:
1) When I write something on fullName input (UserInfo.js), I think React is updating the component with the default value ("") so I cant write anything on it.
Expected behavior:
User can write and input should be saved in datos.fullName on parent component.
When user press Next or back, written information will still be there.
2) Is my approach OK? Should I be saving all children data in parent component and then access it from another child later on (Like Confirm.js or somewhere where the user can read the information before send it). I tried to learn Context API but still don't understand how to implement it 😅
Change your handleChange function like this:
const handleChange = (e) => {
const fieldName = e.target.name;
const fieldValue = e.target.value;
setDatos(prevDatos => ({ ...prevDatos, [fieldName]: fieldValue }));
};
When the new state depends on the previous state you have to use the functional way to update the state.
Regarding your second question, yes, your approach is fine.
And finally, I would maintain some consistency when defining the functions. I'm referring to handleChange and renderComponent functions.

Categories