I feel like I am missing the obvious but I can't seem to find another posts that addresses the issue I am having. I am trying to allow an end user the ability to set a location for an object but either entering the information into a form or by clicking a location on a map. I have a react page with three components.
- A Parent container component that holds the other two components.
import {useState} from 'react';
import Box from '@mui/material/Box';
import ObjSetLocationForm from './ObjSetLocationForm';
import ObjSetLocationMap from './ObjSetLocationMap';
export default function LoadSetLocationSet( props ) {
const [prevLat, setPrevLat] = useState("43.5666694");
const [prevLng, setPrevLng] = useState("-101.0716746");
const [newLat, setNewLat] = useState();
const [newLng, setNewLng] = useState();
function setCoords(coords){
setNewLat(coords.lat);
setNewLng(coords.lng);
}
return(
<Box>
<ObjSetLocationForm prevLat={prevLat} prevLng={prevLng} newLat={newLat} newLng={newLng} setCoordsFx={setCoords} />
<ObjSetLocationMap prevLat={prevLat} prevLng={prevLng} newLat={newLat} newLng={newLng} setCoordsFx={setCoords} />
</Box>
);
}
- A Map component that shows a single point on a map A and allows the end user to click on the map to set the point to a new location (lat/lng) and is a child of the Parent container. I will not post all the code to this component because it is pretty big but the important part is that when a user clicks on the map the new lat/lng values are passed back up to the parent component by calling the setCoords function like so.
props.setCoordsFx({lat: e.lngLat.lat, lng: e.lngLat.lng});
I have verified that this is working correctly and is passing the values correctly.
- A Form component that is the second child of the parent container and allows the end user to enter a Lat/Lng to change the position of the single point on the map. This component is where I am having issues. I have two MUI TextFields in an html form that I want to set to the lat/lng values when the user clicks on the map. When I run through the debugger I can see the values getting passed down to this component from the parent and I can even see that the state values that control the components are getting set but the TextFields values never change.
import {useState, useEffect, useContext} from 'react';
import Box from '@mui/material/Box';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button';
import TextField from '@mui/material/TextField';
export default function LoadSetLocationSet( props ) {
const [newLat, setNewLat] = useState(props.newLat);
const [newLng, setNewLng] = useState(props.newLng);
function handleSubmit(e) {
e.preventDefault();
// Save values to DB.
}
return(
<Box>
<form id="SelLocationForm" onSubmit={handleSubmit}>
<Box sx={{textAlign:'center'}}>
<Typography variant="h6" sx={{display:'inline'}}>Current Location</Typography>
<Box sx={{display:'flex', alignItems:'center', justifyContent:'center'}}>
<Stack direction="row" spacing={3}>
<Box>
<Box>
<Typography>Latitude</Typography>
</Box>
<Box>
<Typography>{props.prevLat}</Typography>
</Box>
</Box>
<Box>
<Box>
<Typography>Longitude</Typography>
</Box>
<Box>
<Typography>{props.prevLng}</Typography>
</Box>
</Box>
</Stack>
</Box>
</Box>
<Box sx={{textAlign:'center', pt:2}}>
<Typography variant="h6" sx={{mb:2}}>Enter the Latitude and Longitude or click the new location on the map</Typography>
<Typography variant="h6" sx={{display:'inline'}}>New Location</Typography>
<Box sx={{display:'flex', alignItems:'center', justifyContent:'center'}}>
<Stack direction="row" spacing={3}>
<Box>
<TextField
id="tbLatitude"
label="Latitude"
type="text"
size="small"
value={newLat}
onChange={(e) => {setNewLat(e.target.value);}}
/>
</Box>
<Box>
<TextField
id="tbLongitude"
label="Longitude"
type="text"
size="small"
value={newLng}
onChange={(e) => {setNewLng(e.target.value);}}
/>
</Box>
<Box sx={{display:'flex', alignItems:'center', justifyItems:'center'}}>
<Button variant="contained" type="submit">Set</Button>
</Box>
</Stack>
</Box>
</Box>
</form>
</Box>
);
}
As you can see I am attempting to use controlled TextFields. So here are my questions/problems:
If setting the default value to a prop value is "anti pattern" how am I supposed to set the default value for form fields if they are a controlled form field?
As I stated earlier when the user clicks on a location on the map it should refresh the form child component and set the values for my controlled form fields to the values passed in but this is not working. How can I accomplish this?
I thought I understood things as I have been doing react for a little bit now but I seem to be lost. Sorry for the newbie question.
CodePudding user response:
In your LoadSetLocationSet
you are passing in newLat
and newLng
as props. You should also pass in setNewLat
and setNewLng
as props.
The problem is that you are defining newLat
and newLng
in two places:
export default function LoadSetLocationSet( props ) {
const [prevLat, setPrevLat] = useState("43.5666694");
const [prevLng, setPrevLng] = useState("-101.0716746");
const [newLat, setNewLat] = useState();
const [newLng, setNewLng] = useState();
and
export default function LoadSetLocationSet( props ) {
const [newLat, setNewLat] = useState(props.newLat);
const [newLng, setNewLng] = useState(props.newLng);
That is going to cause issues since you really only want one source of truth for that data, and things get messy and confusing if you try to keep track of that state in two separate places. Instead, only keep track of newLat
and newLng
in a single place. In React, the flow of data goes downward from parents to children, so we often want to lift state up
Remove these lines from the child form:
const [newLat, setNewLat] = useState(props.newLat);
const [newLng, setNewLng] = useState(props.newLng);
Then in the parent we will pass down setNewLat
and setNewLng
as props to the Form:
<ObjSetLocationForm prevLat={prevLat} prevLng={prevLng} newLat={newLat} newLng={newLng} setCoordsFx={setCoords} setNewLat={setNewLat} setNewLng={setNewLng} />
<ObjSetLocationMap prevLat={prevLat} prevLng={prevLng} newLat={newLat} newLng={newLng} setCoordsFx={setCoords} />
Then you just have to adjust the onChange
functions in your TextFields
to use the setNewLat
and setNewLng
functions that are passed in as props. You will also need to change value
to use the data passed from props in order to set the default/initial value of those fields:
<TextField
id="tbLatitude"
label="Latitude"
type="text"
size="small"
value={props.newLat}
onChange={(e) => {props.setNewLat(e.target.value);}}
/>
</Box>
<Box>
<TextField
id="tbLongitude"
label="Longitude"
type="text"
size="small"
value={props.newLng}
onChange={(e) => {props.setNewLng(e.target.value);}}
/>
The main idea is to keep track of data in only one place, often a parent component. Even though we store data in the parent, we can still update state from a child by passing down a callback to the child as a prop.