I started with react and have a small problem
export default function TestComponent3({ typeId}) {
console.log('CHANGING HERE', { typeId});
const handleClick = () => {
console.log('NOT CHANGING HERE', { typeId});
console.log('NOT CHANGING HERE EITHER', typeId);
};
return (
<>
<Spectrum.Button variant="secondary" onClick={handleClick}>
CHANGING HERE {typeId}
</Spectrum.Button>
</>
);
}
In the other component the state of 'myProp' is changing by the UI dropdown.
Spectrum.Button content changes dynamically but console logs are stuck on default state option.
I probably messed something up and and It's easy fix.
EDIT://
Sibling with a dropdown
export default function TypeForm({ typeId, setTypeId }) {
return (
<div>
<Spectrum.Dropdown className="dropdown" placeholder="Choose type...">
<Spectrum.Menu onChange={setTypeId} slot="options">
{Types.map(type => { //types 0,1,2,3,4,5,6,7,8
return (
<Spectrum.MenuItem selected={typeId === type.id ? true : null} key={type.id} className="jsontest">
{type.name}
</Spectrum.MenuItem>
);
})}
</Spectrum.Menu>
</Spectrum.Dropdown>
)}
</div>
);
}
And Parent
export default function Form() {
const [typeId, setTypeId] = useState(0);
const setTypeIdFunc = e => {
setTypeId(e.target.selectedIndex);
};
return (
<TestComponent3 typeId={typeId} />
<TypeForm
typeId={typeId}
setTypeId={setTypeIdFunc}
/>
)
The props are passed down first from the parent component of TestComponent3 during the first render.
This means you would have to send handleClick from one "higher" component. Or use a shared state-like context provider.
I fixed it by changing Spectrum.Button to normal button.
Related
I wanted to make my components as reusable as it possible but when I started adding events the problems occured. I am using one button component in a lot of places in my app and I just change its name. It worked fine when I passed one onClick event to it (to change menu button name) but when I wanted to do the same with another button (to change cycle name) and when I passed second onClick event to the same button component the menu button stopped working. I tried to find solution but found only different topics. I know I could make a wrapper around the button and make onClick on the wrapper, but I think I am doing something wrong and there must be more elegant way to handle this.
Button component
export const Button = ({text, changeButtonName, changeCycle}) => {
return (
<AppButton onClick={changeButtonName, changeCycle}>
{text}
</AppButton>
);
};
Navbar component where cycle and menuu buttons are placed
export const Navbar = () => {
const menuButton = 'Menu';
const closeButton = 'Zamknij';
const [menuButtonName, setMenuButtonName] = useState(menuButton);
const changeButtonName = () => {
menuButtonName === menuButton ? setMenuButtonName(closeButton) : setMenuButtonName(menuButton);
}
const interiorButton = 'Interior →';
const structuralCollageButton = 'Structural Collage →';
const [cycleButtonName, setCycleButtonName] = useState(interiorButton);
const changeCycle = () => {
cycleButtonName === interiorButton ? setCycleButtonName(structuralCollageButton) : setCycleButtonName(interiorButton);
}
return (
<Nav>
<AuthorWrapper>
<AuthorName>
Michał Król
</AuthorName>
<AuthorPseudonym>
Structuralist
</AuthorPseudonym>
</AuthorWrapper>
<CycleButtonWrapper >
<Button text={cycleButtonName} changeCycle={changeCycle} />
</CycleButtonWrapper>
<MenuButtonWrapper>
<Button text={menuButtonName} changeButtonName={changeButtonName} />
</MenuButtonWrapper>
</Nav>
)
}
this is not a really reusable approach for a Button. For every new method name you would have to include in the props params and you could face something like:
export const Button = ({text, changeButtonName, changeCycle, changeTheme, changeDisplay})
the proper way to make it reusable would be by passing only one handler to your button:
export const Button = ({text, clickHandler}) => {
return (
<AppButton onClick={clickHandler}>
{text}
</AppButton>
);
};
fwiw, the reason you have problem is because at this code onClick={changeButtonName, changeCycle} you are passing multiple expressions with comma operator where the last operand is returned.
You cannot pass two functions to onClick. Either do a conditional check that call that function which is passed or make a wrapper function.
export const Button = ({text, changeButtonName, changeCycle}) => {
return (
<AppButton onClick={changeButtonName || changeCycle}>
{text}
</AppButton>
);
};
or
export const Button = ({text, changeButtonName, changeCycle}) => {
return (
<AppButton
onClick={() => {
changeButtonName && changeButtonName();
changeCycle && changeCycle();
}
}>
{text}
</AppButton>
);
};
update your code like
<AppButton onClick={()=> {
changeButtonName && changeButtonName();
changeCycle && changeCycle();
}}>
{text}
</AppButton>
I try to create a small bug tracking app with React 17. I have BugComponent where users can easily set priority, status, system, and some other properties by clicking on the related icon and select the value from a small popup dialog. It means I have 5-6 small modal dialogs for each bug and I have 20 bugs by default on the page.
With my current implementation, I have a component for displaying and changing priority like this:
export const PrioritySelector = ({priority}) => {
const [open, setOpen] = useState(false);
const [prio, setPrio] = useState(priority);
const handleOnClick = () => {
setOpen(!open);
}
const handleOnSelect = (selectedPriority) => {
setPrio(selectedPriority);
setOpen(false);
}
return (
<div>
<PriorityIcon priority={prio} onClick={handleOnClick} />
<PriorityOptionSelector open={open} onSelect={handleOnSelect} originalValue={prio}/>
</div>
);
}
I also have similar selectors for status and plannedEndDate.
The main BugComponent looks like this:
export const BugComponent = ({bug}) => {
return (
<div className="bug">
<StatusSelector status={bug.status} />
<div>{bug.text}</div>
<PrioritySelector priority={bug.priority} />
<DateSelector plannedEndDate={bug.plannedEndDate} />
<CommentIcon numberOfComments={bug.commentCount} />
</div>
)
}
It works, but the problem is the following:
If I open the dialog with clicking on the icon and then click on other icon of the same bug or even another bug without selecting a value then this modal dialog remains open. So it can happen that a lot of dialogs are open at a time.
Using vanilla JavaScript I would write something like this before open the new dialog:
document.querySelectorAll('.modal-selector.open').classList.remove('open');
How can I close dialogs before opening a new one in React?
You want to keep the information about what modal is currently open in a state variable above the BugComponent.
Do a state variable like this:
const NO_MODAL_EXPANDED = {bugId: "", componentType: ""};
const [expandedItem, setExpandedItem] = useState(NO_MODAL_EXPANDED);
// Then you pass these props down to the BugComponent:
<BugComponent
bug={bug}
hasExpandedItem={expandedItem.bugId === bug.bugId}
expandedItemType={expandedItem.componentType}
setExpandedItem={setExpandedItem}
closeModal={() => setExpandedItem(NoModalExpanded)}
/>;
In BugComponent you do:
export const BugComponent = ({
bug,
hasExpandedItem,
expandedItemType,
setExpandedItem,
closeModal
}) => {
const isExpanded = (name) => hasExpandedItem && expanedItemType === name;
return (
<div className="bug">
<StatusSelector
status={bug.status}
isExpanded={isExpanded("status")}
closeModal={closeModal}
expandItem={() => setExpandedItem({bugId: bug.bugId, componentType: "status"})}
/>
<div>{bug.text}</div>
<PrioritySelector
priority={bug.priority}
isExpanded={isExpanded("priority")}
closeModal={closeModal}
expandItem={() => setExpandedItem({bugId: bug.bugId, componentType: "priority"})}
/>
<DateSelector
plannedEndDate={bug.plannedEndDate}
isExpanded={isExpanded("date")}
closeModal={closeModal}
expandItem={() => setExpandedItem({bugId: bug.bugId, componentType: "date"})}
/>
<CommentIcon
numberOfComments={bug.commentCount}
isExpanded={isExpanded("comment")}
closeModal={closeModal}
expandItem={() => setExpandedItem({bugId: bug.bugId, componentType: "comment"})}
/>
</div>
);
};
And in the modal components you do:
export const PrioritySelector = ({
priority,
isExpanded,
expandItem,
closeItem,
}) => {
const [prio, setPrio] = useState(priority);
const handleOnClick = () => {
if (isExpanded) {
closeItem();
} else {
expandItem();
}
};
const handleOnSelect = (selectedPriority) => {
setPrio(selectedPriority);
closeItem();
};
return (
<div>
<PriorityIcon priority={prio} onClick={handleOnClick} />
<PriorityOptionSelector
open={isExpanded}
onSelect={handleOnSelect}
originalValue={priority}
/>
</div>
);
};
Read through it and see if you understand it. The core thing you need to understand is: you want to express the state in as few variables as possible. The only thing you want to know is: what component, if any, is expanded? Instead of distributing this state over a lot of components, you keep the state in the parent component that is the smallest common denominator.
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.
I am using redux's connect() on App.js and then passing down a dispatcher to a child. The child also receives the state variable as a prop.
The issue is a log statement on the state variable through the prop mapping lags 1 digit behind what is entered. Here are 2 log statements showing this:
SelectDonation.js:19 getState value {donation_amount: "345"}
SelectDonation.js:20 mapstatetoprops value 34
Here is the child component SelectDonation.js:
handleInputChange = inputEvent => {
this.props.dispatch_set_donation_amount(inputEvent.target.value);
console.log("getState value", store.getState());
console.log("mapstatetoprops value", this.props.donation_amount)
};
render() {
return (
<React.Fragment>
<Form>
<input
type='number'
placeholder='Custom Amount'
name='donation_amount'
id='custom_amount'
onChange={(e) => this.handleInputChange(e)}
/>
<Button
primary
onClick={(event) => {
event.preventDefault();
this.props.dispatchChangeCheckoutStep(checkoutSteps.paymentDetails);
console.log(store.getState().checkoutStep)
}}>Next Step
</Button>
</Form>
</React.Fragment>
)
}
}
App.js:
class App extends Component {
render() {
return (
<div className="App">
<header className="App-header">
<Modal
trigger={<Button color='purple'>Donate</Button>}
size='small'
>
{this.props.checkoutStep === checkoutSteps.selectDonation &&
<SelectDonation
dispatch_set_donation_amount = {this.props.dispatch_set_donation_amount}
dispatchChangeCheckoutStep={this.props.dispatchChangeCheckoutStep}
{...this.props} // passes down all the state
/>
}
{this.props.checkoutStep === checkoutSteps.paymentDetails && <PaymentDetails
redux_donation_amount={this.props.redux_donation_amount}
{...this.props}
/>
}
{this.props.checkoutStep === checkoutSteps.referrals && <Referrals dispatchUpdateStateData={this.props.dispatchUpdateStateData} />}
</Modal>
</header>
</div>
);
}
}
const map_state_to_props = (state) => {
return {
log_prop : state.log_to_console,
donation_amount : state.donation_amount,
checkoutStep : state.checkoutStep,
}
};
const map_dispatch_to_props = (dispatch, own_props) => {
return {
dispatch_set_donation_amount : amount => dispatch(set_donation_amount(amount)),
dispatchChangeCheckoutStep : newStep => dispatch(changeCheckoutStep(newStep)),
dispatchUpdateStateData : (stateData, stateVariable) => (dispatch(updateStateData(stateData, stateVariable)))
}
};
//connecting redux
export const AppWrapped = connect(map_state_to_props, map_dispatch_to_props)(App);
It looks correct to me but obviously I am neglecting something. What is causing the mapStateToProps variable to be one keystroke behind persistently?
The component has not updated yet. You have called an action to be dispatched to your reducer, but not waited for the component to receive the new props. You can see this by either pushing the second log into a setTimeout (not recommended)
this.props.dispatch_set_donation_amount(inputEvent.target.value);
console.log("getState value", store.getState());
setTimeout(() => console.log("mapstatetoprops value", this.props.donation_amount));
or by using a middleware such as thunk and returning a Promise from your action creator.
this.props.dispatch_set_donation_amount(inputEvent.target.value).then(x => console.log("mapstatetoprops value", x));
console.log("getState value", store.getState());
Because React has not had a chance to update the component. Your own code is still executing, so the rest of the component lifecycle has not executed yet, and therefore your component still has its existing props.
Also, please don't access the store directly in your components. One of the main purposes of connect is to handle that for you.