I know, this topic has been handeled a lot, but I am still lost in my particular example. I have a react-select component, which is a part of another component, which is a part of App component.
SubjectSelect.tsx
export default function SubjectSelect({handleChange, value}) {
return (
<>
<Select
placeholder="Choose subject"
value={value} // set selected value
onChange={handleChange} // assign onChange function
/>
<div><b>Your choice: </b> {value} </div>
</>
)
}
FormStepOne.tsx
import SubjectSelect from "../components/SubjectSelect";
export const SubjectSelector = ({value, handleChange}) => {
return (
<>
<h1 className="text-3xl font-bold underline">
Hi there! Please select book subject
</h1>
<SubjectSelect value={value} onChange={handleChange}/>
</>
);
}
And App.tsx
import React, { useState } from 'react'
import { SubjectSelector } from '../formSteps/stepOne'
import { ShowcaseBooks } from '../formSteps/stepTwo'
const options = [
{ value: 'fiction', label: 'Fiction' },
{ value: 'science', label: 'Science' },
]
export default function App() {
const [books, setBooks] = useState('')
const [selectedValue, setSelectedValue] = useState('');
const handleChange = e => {
setSelectedValue(e.value);
alert('huhu')
}
const value = options.find(obj => obj.value === selectedValue)
return (
<div className="bg-blue-200">
<div className="container mx-auto py-36 bg-blue-200">
<div className="mt-12 px-96">
<SubjectSelector
options={options}
value={value}
onChange={handleChange}/>
<ShowcaseBooks books={books}/>
</div>
</div>
</div>
)
}
Somehow I am not passing the props right, so that my components showcase several errors. App.tsx complains about options and onChange, but I am lost and don't reallyfully undertand what is wrong and how to pass props corretly, so my App.js and therefore Showcase books "know" the selectdValue.
The problem is you pass the handleChange function to SubjectSelector on the onChange prop, while that component is expecting it on a prop named handleChange.
In App.tsx you need something like this
<SubjectSelector
options={options}
value={value}
handleChange={handleChange}/> // This is the fix
Because when you do this
export const SubjectSelector = ({value, handleChange}) =>
You're telling the component the name of the props to expect. You need to change your Subject selector in a similar manner.
For first loading, the prop value you send is empty (because change event is not triggered). so It will turn your app into error.
So you need to check it not empty before parse it to HTML
SubjectSelect.tsx
export default function SubjectSelect({ options, handleChange, selectedVal }) {
return (
<>
<select
value={selectedVal && selectedVal.value} // set selected value
onChange={handleChange} // assign onChange function
>
<option>Choose subject</option>
{options.map((item: any) => {
return (
<option key={item.value} value={item.value}>
{item.label}
</option>
);
})}
</select>
<div>
<b>Your choice: </b> {selectedVal && selectedVal.value}
</div>
</>
);
}
FormStepOne.tsx
import SubjectSelect from "./SubjectSelect";
const SubjectSelector = ({ selectedVal, handleChange, options }) => {
return (
<>
<h1 className="text-3xl font-bold underline">
Hi there! Please select book subject
</h1>
<SubjectSelect
options={options}
selectedVal={selectedVal}
handleChange={handleChange}
/>
</>
);
};
export default SubjectSelector;
And do not use value as variable name or prop to avoid conflict with key of object.
Getting value from change event should be e.target.value
import React, { useState } from "react";
import SubjectSelector from "./SubjectSelect";
const options = [
{ value: "fiction", label: "Fiction" },
{ value: "science", label: "Science" }
];
export default function App() {
const [selectedValue, setSelectedValue] = useState('');
const handleChange = (e: any) => {
setSelectedValue(e.target.value);
alert(e.target.value);
};
const selectVal = options.filter((obj) => obj.value === selectedValue)[0];
return (
<div className="bg-blue-200">
<div className="container mx-auto py-36 bg-blue-200">
<div className="mt-12 px-96">
<SubjectSelector
options={options}
selectedVal={selectVal}
handleChange={handleChange}
/>
</div>
</div>
</div>
);
}
You can check my complete answer here. I hope it might helps you
https://codesandbox.io/s/delicate-smoke-n0eyjc?file=/src/App.tsx:0-804
Related
The Goal
I'm building a two-sided marketplace for buyers and sellers. When someone navigates to my signup page, they could hit a URL like one of the following
/signup
/signup?accountType=buyer
/signup?accountType=seller
/signup?accountType=asdfghjkl (nonsensical, but possible)
My signup page has a radio button input where they choose Buyer or Seller.
Rules
The user must select one of these options.
If the URL contains accountType=buyer, I would like to set the default selection as Buyer
If the URL contains accountType=seller, I would like to set the default selection as Seller
Even if a default option is selected, the user should be able to change it
What I've tried
I am struggling to make this work with Next.js and react-hook-form. Here's what I've tried.
// Fields.jsx
import { forwardRef } from 'react'
function Label({ id, children }) {
return (
<label htmlFor={id}>
{children}
</label>
)
}
export const RadioFieldWithRef = forwardRef(function RadioField({ id, label, options, name, className = '', ...props }, ref) {
return (
<div className={className}>
{label && <Label id={id}>{label}</Label>}
<div>
{options.map((option) => (
<div className="flex items-center" key={option.value}>
<input
id={option.value}
name={name}
type="radio"
value={option.value}
defaultChecked={option.defaultChecked}
ref={ref}
{...props}
/>
<label htmlFor={option.value}>
{option.label}
</label>
</div>
))}
</div>
</div>
)
})
// signup.jsx
import { useRouter } from 'next/router'
import { RadioFieldWithRef } from '#/components/Fields'
import { useForm } from "react-hook-form";
export default function Signup() {
const router = useRouter()
const { accountType } = router.query
// Email & Password Sign Up Form
const { register } = useForm();
const accountTypeField = register("account_type", {
required: "Must select account type"
})
return (
<form>
<RadioFieldWithRef
label="I'm a ..."
name="account_type"
options={ [
{
label: 'Buyer',
value: 'buyer',
defaultChecked: accountType === "buyer"
},
{
label: 'Seller',
value: 'seller',
defaultChecked: accountType === "seller"
},
] }
{...accountTypeField}
/>
<button type="submit">Submit</button>
</form>
)
}
The Problem
When I try a URL like /signup?accountType=buyer, the default selection is not being set. I think this is because router.query is actually undefined on the first render. console.log("accountType", accountType) shows undefined before it eventually shows buyer. But I'm not sure how to overcome this.
Change:
const { accountType } = router.query
To: const [accountType, setAccountType] = useState('')
And add this:
useEffect(() => {
if (router.query) { setAccountType(router.query.accountType) }
}, [router.query])
You're right that the problem is because the params aren't loaded in time to set the accountType, so you can use the useEffect hook to only set the accountType constant once the router.query is available.
Have you tried setting the values like so?
const { register } = useForm({values: {
"account_type": accountType
}});
My suggestion is to move the form in a different component, and only show it after the router.isReady is true. See https://nextjs.org/docs/api-reference/next/router#router-object for how to use the isReady flag.
Got this to work with some help from leapful on GitHub.
You need to watch on change of router.query by using useEffect hook then use setValue() method from useForm() hook to set value for field account_type.
Here's what my resulting code looks like
// Fields.jsx
import { forwardRef } from 'react'
function Label({ id, children }) {
return (
<label htmlFor={id}>
{children}
</label>
)
}
export const RadioFieldWithRef = forwardRef(function RadioField({ id, label, options, name, className = '', ...props }, ref) {
return (
<div className={className}>
{label && <Label id={id}>{label}</Label>}
<div>
{options.map((option) => (
<div className="flex items-center" key={option.value}>
<input
id={option.value}
name={name}
type="radio"
value={option.value}
ref={ref}
{...props}
/>
<label htmlFor={option.value}>
{option.label}
</label>
</div>
))}
</div>
</div>
)
})
// signup.jsx
import { useRouter } from 'next/router'
import { RadioFieldWithRef } from '#/components/Fields'
import { useForm } from "react-hook-form";
export default function Signup() {
const router = useRouter()
// Email & Password Sign Up Form
const { register, setValue } = useForm();
const accountTypeField = register("account_type", {
required: "Must select account type"
})
useEffect(() => {
if(router.query?.accountType === "buyer" || router.query?.accountType === "seller"){
setValue("account_type", router.query.accountType)
}
}, [router.query])
return (
<form>
<RadioFieldWithRef
label="I'm a ..."
name="account_type"
options={ [
{
label: 'Buyer',
value: 'buyer',
},
{
label: 'Seller',
value: 'seller',
},
] }
{...accountTypeField}
/>
<button type="submit">Submit</button>
</form>
)
}
I have 3 files that make up a component. I am trying to calculate a bmi result which is split into several files. When I click calculate, the page changes. I have tried to find that I am new to React in general, and these are things that are probably easy but not yet obvious to me
import React, { useState } from "react";
import style from './BMI.module.css';
import Calculator from './Calculator'
import Result from './Result'
function BMI() {
const [page, setPage] = useState(0);
const checkPage = () => {
if (page === 0) {
return (
<button className={`${style.button}`}
disabled={page === 1}
onClick={() => {
setPage((currPage) => currPage + 1);
}}>Calculate</button>
)
} else {
return (
<button className={`${style.button}`} disabled={page === 0} onClick={() => {
setPage((currPage) => currPage - 1);
}}>Back</button>
)
}
}
const PageDisplay = () => {
if (page === 0) {
return <Calculator />;
} else {
return <Result />;
}
};
return (
<div className={`${style.wrapper}`}>
<div className={`${style.box}`}>
<div className={`${style.img}`}></div>
<div className={`${style.topSection}`}>
<h1 className={`${style.title}`}>{(page === 0 ? 'BMI Calculator' : 'Result')}</h1>
</div>
<div className="body">{PageDisplay()}</div>
<div>{checkPage()}</div>
</div>
</div>
)
}
export default BMI
import React, { useState } from "react";
import style from './Calculator.module.css'
import { FontAwesomeIcon } from '#fortawesome/react-fontawesome';
import { faMars, faVenus } from '#fortawesome/free-solid-svg-icons';
const Input = ({ label, id, handleChange, name, type, placeholder }) => (
<>
<label className={`${style.label}`} htmlFor={id}>{label}</label>
<input className={`${style.input}`} type={type || "number"} id={id} name={name || id} placeholder={placeholder} onChange={(e) => handleChange(e.target.value)}></input>
<br />
</>
);
function Calculator() {
const [height, setHeight] = useState(0)
const [weight, setWeight] = useState(0)
const [age, setAge] = useState(0)
function removeSelected() {
const selectedBox = document.querySelectorAll('#head')
console.log(selectedBox.classList)
selectedBox.forEach(item => {
item.classList.remove(`${style.activeBox}`)
})
}
const handleToggle = (e) => {
if (!e.currentTarget.className.includes("activeBox")) {
removeSelected()
e.target.classList.add(`${style.activeBox}`)
} else {
e.target.classList.remove(`${style.activeBox}`)
}
};
return (
<>
<div className={`${style.content}`}>
<div className={`${style.middleSection}`}>
<h3 className={`${style.formTitle}`}>Choose your gender</h3>
<div className={`${style.genders}`}>
<div id="head" onClick={handleToggle} className={`${style.genderBox}`}>
<FontAwesomeIcon icon={faMars} className={`${style.genderIcon}`} />
<h3 className={`${style.genderBoxTitle}`}>Male</h3>
</div>
<div id="head" onClick={handleToggle} className={`${style.genderBox}`}>
<FontAwesomeIcon icon={faVenus} className={`${style.genderIcon}`} />
<h3 className={`${style.genderBoxTitle}`}>Female</h3>
</div>
</div>
</div>
<div className={`${style.bottomSection}`}>
<Input handleChange={setWeight} placeholder='Weight' label='Your weight (kg)'>{weight}</Input>
<Input handleChange={setHeight} placeholder='Height' label='Your height (cm)'>{height}</Input>
<Input handleChange={setAge} placeholder='Age' label='Your age'>{age}</Input>
</div>
</div>
</>
)
}
export default Calculator
import React, { useState } from 'react'
import style from './Result.module.css'
function Result() {
const [bmiScore, setBmiScore] = useState(0)
const [bmiDesc, setBmiDesc] = useState('')
return (
<div className={`${style.content}`}>
<div className={`${style.img}`}></div>
<div className={`${style.descriptions}`}>
<p className={`${style.bmiScoreDesc}`}>Your BMI is <span className={`${style.bmiScoreNumber}`}>{bmiScore}</span>, indication your weight is in the <span className={`${style.bmiScoreDesc}`}>{bmiDesc}</span> category for adults of your height</p>
<p className={`${style.descriptionBottom}`}>Maintaining a healthy weight may reduce the risk of chronic diseases associated with overweight and obesity.</p>
</div>
</div>
)
}
export default Result
So when i click Calculate which i have in the BMI.js take inputs value from Calculator.js calculate the score and put into paragraph in Result.js
What you will want to do in this case is at the top level, in BMI, that's where your bmiScore state variable should live. You can pass it to both toe Calculator (where it will be calculated) and the Result (where it will be displayed) via props, like so:
<Calculator bmi={bmiScore} /> and <Result bmi={bmiScore} />
Then the method signatures in each will look like:
function Calculator({ bmi }) and function Result({ bmi })
And you can use that bmi variable in both.
This is all because React passes variables down, not up. The only way to share across components is if a parent component holds the variable.
i send a usestate thro AppContexet.Provider to AppContexet.Consumer,
i can see at the components - the data arived but when i tray to map the state i get an erorr.
what m i doing wrong?
this is the perent :
import {useState} from 'react'
import Ex53Child from './Ex53Child'
import AppContexet from './Appcontext'
import React, {Component} from 'react';
const Ex53perent = ()=>
{
const [name,setName] = useState('')
const [age,setAge] = useState()
const [users,setUsers] = useState ([])
const [user,setUser] = useState ({name : '' , age : ''})
return(<AppContexet.Provider value = {{...users}}>
<div>
<h1> Ex53perent comp </h1>
Name: <input type = "text" onChange = {e=>setUser({...user ,name : e.target.value})}/><br/>
Age : <input type = "text" onChange = {e=>setUser({...user ,age : e.target.value})}/><br/>
<input type = "button" value ="Add" onClick ={e=>setUsers([...users,user])}/>
<Ex53Child/>
</div>
</AppContexet.Provider>
)
}
export default Ex53perent;
this is the child :
import {useState} from 'react'
import AppContexet from './Appcontext'
import Ex53GrenChild from './Ex53GrenChild'
const Ex53Child = ()=>
{
const [myUesr,setMyUser] = useState([])
return(<AppContexet.Consumer>
{
dataContext =>
(
<div>
<h1> Ex53Child comp </h1>
{
dataContext.users.map((item,index)=>
{
return<il key = {index}>
<li>{item.name}</li>
<li>{item.age}</li>
</il>
})
}
<Ex53GrenChild/>
</div>
)
}
</AppContexet.Consumer>
)
}
export default Ex53Child;
and this is the appcontext file:
import React from 'react'
const AppContexet = React.createContext();
export default AppContexet;
a singel value work just fine
but i cant map the arry from some resen
Issue
The users state is an array:
const [users, setUsers] = useState([]);
And you are spreading the array into an object:
<AppContexet.Provider value={{ ...users }}>
...
</AppContexet.Provider>
This makes the context value an object where the array indices are now object keys and the array values are object values.
Solutions
Convert context value to array
You could keep it this way, and then you'll need to convert the context value back to an array in the child:
<AppContexet.Consumer>
{dataContext => (
<div>
<h1>Ex53Child comp</h1>
{Object.values(dataContext).map((item, index) => {
return (
<il key={index}>
<li>{item.name}</li>
<li>{item.age}</li>
</il>
);
})
}
<Ex53GrenChild/>
</div>
)}
</AppContexet.Consumer>
Fix context value, keep it an array
It's better to just keep the users and context values consistently an array. Don't spread the users state into the context value.
<AppContexet.Provider value={{ users }}>
...
</AppContexet.Provider>
Now's the context value is an object with a single users property that is the users state.
<AppContexet.Consumer>
{dataContext => (
<div>
<h1>Ex53Child comp</h1>
{dataContext.users.map((item, index) => {
return (
<il key={index}>
<li>{item.name}</li>
<li>{item.age}</li>
</il>
);
})
}
<Ex53GrenChild/>
</div>
)}
</AppContexet.Consumer>
Use useContext hook
Since Ex53Child is a function component it's a bit silly to use the AppContexet.Consumer component render function. Use the useContext hook instead.
const Ex53Child = () => {
const [myUesr, setMyUser] = useState([]);
const { users } = useContext(AppContexet);
return (
<div>
<h1>Ex53Child comp</h1>
{users.map((item, index) => (
<il key={index}>
<li>{item.name}</li>
<li>{item.age}</li>
</il>
))}
<Ex53GrenChild />
</div>
);
}
Just pass users as a prop
Additionally, since Ex53perent is directly rendering Ex53Child, using the context is unnecessary complexity when the users state could just simply be passed as a prop.
const Ex53perent = () => {
const [name, setName] = useState('');
const [age, setAge] = useState();
const [users, setUsers] = useState([]);
const [user, setUser] = useState({ name: '', age: '' });
return (
<div>
<h1> Ex53perent comp </h1>
Name:{" "}
<input
type="text"
onChange={e => setUser({ ...user, name: e.target.value })}
/>
<br/>
Age:{" "}
<input
type="text"
onChange={e => setUser({ ...user, age: e.target.value })}
/>
<br/>
<input
type="button"
value="Add"
onClick={e => setUsers([...users, user])}
/>
<Ex53Child users={users} /> // <-- pass users as prop to child
</div>
);
};
...
const Ex53Child = ({ users }) => { // <-- access users from props
const [myUesr, setMyUser] = useState([]);
return (
<div>
<h1>Ex53Child comp</h1>
{users.map((item, index) => (
<il key={index}>
<li>{item.name}</li>
<li>{item.age}</li>
</il>
))}
<Ex53GrenChild />
</div>
);
}
I have designed a component MyCheckBox ( i used it as helper component ), so I imported it in another component, but whenever i am trying to trigger the event, it not triggered.
Code for reference
MyCheck Box
interface MyCheckBoxProps
{
onClick?:MouseEventHandler<HTMLInputElement>;
checked?:boolean
}
export const MyCheckBox (props:MyCheckBoxProps): JSX.Element =>{
return(
<input
type="checkbox"
id={id}
onClick={onClick}
checked={checked}
/>
)
}
When I Imported it
const[check,setCheck] = useState(false)
const handleCheckChange = (e: any, key: string) => {
console.log("check", e) // => this console log does not show up
if (key === "newPostActivity") {
console.log("check", e)
setCheck(e.target.checked)
}
}
<MyCheckBox checked={check} onClick={(e)=>handleCheckChange(e,"some string")}/>
Whenever I click on MyCheckBox , the event ( handleCheckChange )is never triggered
Anybody please help?
you are calling
<MyComponent checked={check}
onClick={(e) => handleCheckChange(e, "some string")}/>
in your App component which is the parent component. Here your checked and onclick are the props. so now accessing the props in your <MyComponent you have to make sure to call it as
import React from "react";
export default function MyCheckBox(props) {
return (
<input type="checkbox" onClick={props.onClick} checked={props.checked} />
);
}
OR
destructrize your props like
import React from "react";
export default function MyCheckBox({onClick, checked}) {
return (
<input type="checkbox" onClick={onClick} checked={checked} />
);
}
I don't understand if you wrote this code as an example of a not working one. If this is the final code, you must destruct the props to use like that.
<input
type="checkbox"
id={id}
onClick={onClick}
checked={checked}
/>
Otherwise, you should call:
<input
type="checkbox"
id={props.id}
onClick={props.onClick}
checked={props.checked}
/>
I've rewrote your code:
App
export default function App() {
const [check, setCheck] = useState(false);
const handleCheckChange = (e: MouseEvent<HTMLInputElement>, key: string) => {
console.log("check", e);
setCheck(!check);
console.log("I am state", check);
};
return (
<div>
<MyCheckBox
id="foo"
checked={check}
onClick={(e) => handleCheckChange(e, "some string")}
/>
</div>
);
}
MyCheckBox
interface MyCheckBoxProps {
id: string;
onClick?: MouseEventHandler<HTMLInputElement>;
checked?: boolean;
}
const MyCheckBox: React.FC<MyCheckBoxProps> = (props) => {
const { id, onClick, checked } = props;
return <input type="checkbox" id={id} onClick={onClick} checked={checked} />;
};
export default MyCheckBox;
Copied from my codesandbox
When I make a selection from the dropdown I saved the selected value to type then when I click the button I add an object to drums, I map over thee drums and based on the type I want to render the component with the same name.
Sandbox here
import React, { useState } from "react";
import uuid from "react-uuid";
import "./styles.css";
const Snare = () => {
return <div>Snare</div>;
};
const Gong = () => {
return <div>Gong</div>;
};
export default function App() {
const [drums, setDrums] = useState([]);
const [type, setType] = useState();
return (
<div className="App">
{drums.map((Drum, index) => (
<Drum.type /> // Why cant I use `.type`?
))}
<label>
Drum type to add:
<select onChange={e => setType(e.target.value)} value={type}>
<option value="">Select...</option>
<option value="Snare">Snare</option>
<option value="Gong">Gong</option>
</select>
<button
onClick={() => {
setDrums([...drums,
{
id: uuid(),
type
}
]);
}}
>
Add drum
</button>
</label>
</div>
);
}
In your case Drum.type is not a component but a string, you need to maintain a map of the string to component and then render it
const map = {
Snare: Snare,
Gong: Gong
};
export default function App() {
const [drums, setDrums] = useState([]);
const [type, setType] = useState();
return (
<div className="App">
{drums.map((Drum, index) => {
const Component = map[Drum.type];
return <Component key={index}/>;
})}
<label>
Drum type to add:
<select onChange={e => setType(e.target.value)} value={type}>
<option value="">Select...</option>
<option value="Snare">Snare</option>
<option value="Gong">Gong</option>
</select>
<button
onClick={() => {
setDrums([
...drums,
{
id: uuid(),
type
}
]);
}}
>
Add drum
</button>
</label>
</div>
);
}
Working demo
That's because the type is a string.
You could create a mapping to solve this and use React.createElement().
Something like:
const mapping = {
'Snare': Snare,
'Gong': Gong
}
{ drums.map(({ type }, index) => (
React.createElement(mapping[type], { key: index })
))
}