I want to handle the click of my custom button component in two separate conditions.
This is the custom-button component:
function CustomButton({status, onStop, onClick}) {
return (
// ...
<button onClick={status ? onStop : onClick}/>
// ...
)
}
This code is just a simple example to explain my question. and I can't separate my component according to the value of status.
My question is, Is this an anti-pattern or bad practice in a component? If yes, what's the best practice to do?
It's not an anti-pattern and more like a bad practice, such code isn't maintainable for when more conditions and callbacks will be added.
Components should be simple, readable and reusable while using all provided props as possible (i.e don't add unused props to component's logic):
const NOOP = () => {}
function CustomButton({ onClick = NOOP }) {
return <button onClick={onClick}>Cool Button</button>;
}
Better practice is to handle condition in parent's logic:
function Parent() {
const onContinue = // ...
const onStop = // ...
return <CustomButton onClick={status ? onStop : onContinue} />;
}
I think that is better to pass a single callback onClick and handle the business logic inside it.
Create a function handleClick inside CustomButton component before return and use if-else blocks to achieve this functionality. It is the right way, according to me.
Create your custom button and use it into any place with any props
import React from 'react';
const CustomButton = ({ children, type = 'button', onClick }) => (
<button
type={type}
onClick={onClick}
>
{children}
</button>
);
const Parent = ({ status }) => {
const stop = useCallback(() => {}, []);
const onClick = useCallback(() => {}, []);
return (
status
? <CustomButton onClick={useCallback}>stop</CustomButton>
: <CustomButton onClick={onClick}>click</CustomButton>
);
}
Related
I am new to React and have had some hard time to understand the concept of states.
Down below I export a stepper from MUI. I use state
export default function CustomizedSteppers() {
const steps = ['Zoninfo', 'Betalsätt', 'Börja ladda'];
const [activeStep, setActiveStep] = React.useState(0);
const handleNext = () => {
setActiveStep((activeStep+1));
};
return (
//...
//Stepper stuff...
//...
<Button variant="contained" onClick={handleNext}>Hello
World</Button>
</Stack>
);
}
I now want to split this code so that I can setActiveStep from another component.
Meaning, I want to put the button outside of this component and put it in another class, but still allow that button to change the value of activeStep - by accessing the method handleNext on click on a button outside this class. How do I manage to do this?
You need to make a parent component, define activeStep and handleNext there, and pass them to your CustomizedSteppers
Parent.js
import CustomizedSteppers from "./CustomizedSteppers"
export default function Parent() {
const steps = ['Zoninfo', 'Betalsätt', 'Börja ladda'];
const [activeStep, setActiveStep] = React.useState(0);
const handleNext = () => {
setActiveStep((activeStep+1));
};
return (
<>
<CustomizedSteppers activeStep={activeStep} handleNext={handleNext}>
<Button variant="contained" onClick={handleNext}>Hello
World</Button>
</>
)
}
CustomizedSteppers.js
export default function CustomizedSteppers({activeStep, handleNext}) {
return (
//...
//Stepper stuff...
//...
</Stack>
);
}
You have a couple options.
The first is to lift your state, like #Peter said. That is, you'll need to move activeStep to the component that houses both this component and the other component you want to have activeStep's value, and then pass that value to both (a practice known as prop drilling). This is probably the simplest way, if this is the only variable you want to do that for.
The second is to use state management tools, like #ahmetkilinc said. The Context API is native to React, so it won't require any extra installations or tools. There are also third-party tools like Redux that try to solve this problem (though I believe Redux is still just using Contexts behind the scenes, anyway). This option is better if this is a need you anticipate having multiple times throughout your application.
You can just pass the state updating function as a prop to your component.
Your Button Component:
function CustomButtonComponent (props) {
return (
<button onClick={() => props.setCount(count => count + 1)}>
Click me
</button>
)
}
export default CustomButtonComponent
And In Your Main Component
<CustomButtonComponent setCount={setCount} />
Also Since state updates are scheduled in React, you shouldnt use
onClick={() => setCount(count + 1)}
Instead use
onClick={() => setCount(prevCount => prevCount + 1)}
Try understanding following code .
const Child = (props)=>{
return (
<Button variant="contained" onClick={props.handleNext}>Hello
World</Button>
)
}
const Parent = ()=>{
const steps = ['Zoninfo', 'Betalsätt', 'Börja ladda'];
const [activeStep, setActiveStep] = React.useState(0);
const handleNext = () => {
setActiveStep((activeStep+1));
};
return (
<Child handleNext={(e)=>handleNext(e)} />
)
}
Take this code as a sample example and try implementing it with your code .
What is happening in this Parent Function ?
We are passing handleNext as a prop to to Child component which triggers handleNext function inside Parent component when a button is clicked inside Child component.
I am new to React and have had some hard time to understand the concept of states.
Down below I export a stepper from MUI. I use state
export default function CustomizedSteppers() {
const steps = ['Zoninfo', 'Betalsätt', 'Börja ladda'];
const [activeStep, setActiveStep] = React.useState(0);
const handleNext = () => {
setActiveStep((activeStep+1));
};
return (
//...
//Stepper stuff...
//...
<Button variant="contained" onClick={handleNext}>Hello
World</Button>
</Stack>
);
}
I now want to split this code so that I can setActiveStep from another component.
Meaning, I want to put the button outside of this component and put it in another class, but still allow that button to change the value of activeStep - by accessing the method handleNext on click on a button outside this class. How do I manage to do this?
You need to make a parent component, define activeStep and handleNext there, and pass them to your CustomizedSteppers
Parent.js
import CustomizedSteppers from "./CustomizedSteppers"
export default function Parent() {
const steps = ['Zoninfo', 'Betalsätt', 'Börja ladda'];
const [activeStep, setActiveStep] = React.useState(0);
const handleNext = () => {
setActiveStep((activeStep+1));
};
return (
<>
<CustomizedSteppers activeStep={activeStep} handleNext={handleNext}>
<Button variant="contained" onClick={handleNext}>Hello
World</Button>
</>
)
}
CustomizedSteppers.js
export default function CustomizedSteppers({activeStep, handleNext}) {
return (
//...
//Stepper stuff...
//...
</Stack>
);
}
You have a couple options.
The first is to lift your state, like #Peter said. That is, you'll need to move activeStep to the component that houses both this component and the other component you want to have activeStep's value, and then pass that value to both (a practice known as prop drilling). This is probably the simplest way, if this is the only variable you want to do that for.
The second is to use state management tools, like #ahmetkilinc said. The Context API is native to React, so it won't require any extra installations or tools. There are also third-party tools like Redux that try to solve this problem (though I believe Redux is still just using Contexts behind the scenes, anyway). This option is better if this is a need you anticipate having multiple times throughout your application.
You can just pass the state updating function as a prop to your component.
Your Button Component:
function CustomButtonComponent (props) {
return (
<button onClick={() => props.setCount(count => count + 1)}>
Click me
</button>
)
}
export default CustomButtonComponent
And In Your Main Component
<CustomButtonComponent setCount={setCount} />
Also Since state updates are scheduled in React, you shouldnt use
onClick={() => setCount(count + 1)}
Instead use
onClick={() => setCount(prevCount => prevCount + 1)}
Try understanding following code .
const Child = (props)=>{
return (
<Button variant="contained" onClick={props.handleNext}>Hello
World</Button>
)
}
const Parent = ()=>{
const steps = ['Zoninfo', 'Betalsätt', 'Börja ladda'];
const [activeStep, setActiveStep] = React.useState(0);
const handleNext = () => {
setActiveStep((activeStep+1));
};
return (
<Child handleNext={(e)=>handleNext(e)} />
)
}
Take this code as a sample example and try implementing it with your code .
What is happening in this Parent Function ?
We are passing handleNext as a prop to to Child component which triggers handleNext function inside Parent component when a button is clicked inside Child component.
This is my application with the scenario reproduced, here the demo in codesandbox
I have two components, Leagues ( parent ) and Details ( Child ).
I have a implemented reset button example in the Details Component button which does
const cleanArray = () => {
setDataHomeTeam([]);
};
<button onClick={cleanValue} type="button">Reset</button>
You can see in the demo that is emptying out an array of a team stat
My question is, can i implement the same button but out from Details component and from the parent component Leagues for example? Whats the way to achieve it?
I thought to go this way but i can not get it done.
So in my Details.js
let Details = forwardRef(({ ....
const cleanArray = () => {
setDataHomeTeam([]);
};
useImperativeHandle(ref, () => {
return {
cleanValue: cleanValue
}
});
in App.js
<Leagues/>
<button onClick={cleanValue} type="button">Reset</button>
<Details ref={ref} />
I get this error : 'cleanValue' is not defined no-undef
is it something that i can not do with react? How can i achieve it?
I think your approach sounds correct except for lacking the way of calling the api cleanValue you exposed. Basically you have to call it via a ref you pass to it as following:
function App() {
const ref = useRef();
return (
<>
<Leagues />
{/* call it via ref */}
<button onClick={() => ref.current.cleanValue()} type="button">Reset</button>
<Details ref={ref} />
</>
)
}
Codesandbox link: https://codesandbox.io/s/nifty-raman-c0zff?file=/src/components/Details.js
I don't completely understand what you are trying to do, but here is a solution I think is going to work for your problem
let's say you wanna filter that array with the selected team which is liverpool, first if you have control over the incoming data I recommend changing the obj in the array likethis
{day : 16 , teamName:"liverpool"}, this is going to help you filter that array later,
then you useEffect & useState to update that array
[teams, setTeams] = useState([]);
// the array changes here {day: 1 , teamName : "sao paulo"} , {day:2 ,teamname:"liverpool"}]
useEffect(()=>{
setTeams((T)=>{
return T.filter((oneTeam)=>{
return oneTeam.teamName == selectedTeam;
});
})
},[teams,selectedTeam]);
I have a parent component with an if statement to show 2 different types of buttons.
What I do, on page load, I check if the API returns an array called lectures as empty or with any values:
lectures.length > 0 ? show button A : show button B
This is the component, called main.js, where the if statement is:
lectures.length > 0
? <div onClick={() => handleCollapseClick()}>
<SectionCollapse open={open} />
</div>
: <LectureAdd dataSection={dataSection} />
The component LectureAdd displays a + sign, which will open a modal to create a new Lecture's title, while, SectionCollapse will show an arrow to show/hide a list of items.
The logic is simple:
1. On page load, if the lectures.lenght > 0 is false, we show the + sign to add a new lecture
OR
2. If the lectures.lenght > 0 is true, we change and show the collpase arrow.
Now, my issue happens when I add the new lecture from the child component LectureAdd.js
import React from 'react';
import { Form, Field } from 'react-final-form';
// Constants
import { URLS } from '../../../../constants';
// Helpers & Utils
import api from '../../../../helpers/API';
// Material UI Icons
import AddBoxIcon from '#material-ui/icons/AddBox';
export default ({ s }) => {
const [open, setOpen] = React.useState(false);
const [ lucturesData, setLecturesData ] = React.useState(0);
const { t } = useTranslation();
const handleAddLecture = ({ lecture_title }) => {
const data = {
"lecture": {
"title": lecture_title
}
}
return api
.post(URLS.NEW_COURSE_LECTURE(s.id), data)
.then(data => {
if(data.status === 201) {
setLecturesData(lucturesData + 1) <=== this doesn't trigger the parent and the button remains a `+` symbol, instead of changing because now `lectures.length` is 1
}
})
.catch(response => {
console.log(response)
});
}
return (
<>
<Button variant="outlined" color="primary" onClick={handleClickOpen}>
<AddBoxIcon />
</Button>
<Form
onSubmit={event => handleAddLecture(event)}
>
{
({
handleSubmit
}) => (
<form onSubmit={handleSubmit}>
<Field
name='lecture_title'
>
{({ input, meta }) => (
<div className={meta.active ? 'active' : ''}>
<input {...input}
type='text'
className="signup-field-input"
/>
</div>
)}
</Field>
<Button
variant="contained"
color="primary"
type="submit"
>
ADD LECTURE
</Button>
</form>
)}
</Form>
</>
)
}
I've been trying to use UseEffect to trigger a re-render on the update of the variable called lucturesData, but it doesn't re-render the parent component.
Any idea?
Thanks Joe
Common problem in React. Sending data top-down is easy, we just pass props. Passing information back up from children components, not as easy. Couple of solutions.
Use a callback (Observer pattern)
Parent passes a prop to the child that is a function. Child invokes the function when something meaningful happens. Parent can then do something when the function gets called like force a re-render.
function Parent(props) {
const [lectures, setLectures] = useState([]);
const handleLectureCreated = useCallback((lecture) => {
// Force a re-render by calling setState
setLectures([...lectures, lecture]);
}, []);
return (
<Child onLectureCreated={handleLectureCreated} />
)
}
function Child({ onLectureCreated }) {
const handleClick = useCallback(() => {
// Call API
let lecture = callApi();
// Notify parent of event
onLectureCreated(lecture);
}, [onLectureCreated]);
return (
<button onClick={handleClick}>Create Lecture</button>
)
}
Similar to solution #1, except for Parent handles API call. The benefit of this, is the Child component becomes more reusable since its "dumbed down".
function Parent(props) {
const [lectures, setLectures] = useState([]);
const handleLectureCreated = useCallback((data) => {
// Call API
let lecture = callApi(data);
// Force a re-render by calling setState
setLectures([...lectures, lecture]);
}, []);
return (
<Child onLectureCreated={handleLectureCreated} />
)
}
function Child({ onLectureCreated }) {
const handleClick = useCallback(() => {
// Create lecture data to send to callback
let lecture = {
formData1: '',
formData2: ''
}
// Notify parent of event
onCreateLecture(lecture);
}, [onCreateLecture]);
return (
<button onClick={handleClick}>Create Lecture</button>
)
}
Use a central state management tool like Redux. This solution allows any component to "listen in" on changes to data, like new Lectures. I won't provide an example here because it's quite in depth.
Essentially all of these solutions involve the same solution executed slightly differently. The first, uses a smart child that notifies its parent of events once their complete. The second, uses dumb children to gather data and notify the parent to take action on said data. The third, uses a centralized state management system.
Hello I had to add a method inside a component, that was stateless functional component. Now I am wondering if it can stay like this or should it be a class component now. My component:
const Pagination = ({ changePage }) => {
function changePageNumber(event) {
changePage(event.currentTarget.dataset.num);
}
return (
<button
className={css.button}
data-num={num}
onClick={changePageNumber}
>
{num}
</button>
);
};
Can it be like this?
Yes, you can write methods like this. Also you can use arrow functions, like:
const Pagination = ({ changePage }) => {
const changePageNumber = event => {
changePage(event.currentTarget.dataset.num);
}
return (
<button
className={css.button}
data-num={num}
onClick={changePageNumber}
>
{num}
</button>
);
};
Bonus: It's not necessary to name component that exported default, just:
export default ({ changePage }) => {
...
}
And in another file:
import AnyName from './Pagination'
You can change it to be like
const changePageNumber = (event) = () => {
changePage(event.currentTarget.dataset.num);
}
const Pagination = ({ changePage }) => {
return (
<button
className={css.button}
data-num={num}
onClick={changePageNumber}
>
{num}
</button>
);
};
changePageNumber is a function not a method. It is perfectly fine to stay there. It is there as a utility/helper function for that Pagination component. Such functions can be deployed to improve the readability of the code. Current state of your code perfectly fine.
Also you don't need to turn it into Class components, we use them when we think we need to store state, not to store methods.