How to render a different component with React Hooks - reactjs

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.

Related

React - Resetting children state when parent changes its state in functional components

I'm working with a list of notes in React Native, and I was using a bad-performant method to select/deselect the notes when I'm "edit mode". Everytime I selected a note, the application had to re-render the entire list everytime. If I do a test with 100 notes, I get input lags when I select/deselect a note, obviously.
So I decided to move the "select state" to the single Child component. By doing this, I'm having the re-render only on that component, so it's a huge improvement of performance. Until here, everything's normal.
The problem is when I'm disabling edit mode. If I select, for example, 3 notes, and I disable the "edit mode", those notes will remain selected (indeed also the style will persist). I'd like to reset the state of all the selected note, or finding a valid alternative.
I recreated the scene using React (not React Native) on CodeSandbox with a Parent and a Child: https://codesandbox.io/s/loving-field-bh0k9k
The behavior is exactly the same. I hope you can help me out. Thanks.
tl;dr:
Use-case:
Go in Edit Mode by selecting a note for .5s
Select 2/3 elements by clicking on them
Disable Edit Mode by selecting a note for .5s
Expectation: all elements get deselected (state of children resetted)
Reality: elements don't get deselected (state of children remains the same)
this is easy enough to do with a useEffect hook.
It allows you to "watch" variable changes over time.
When editMode changes the contents of the Effect hook runs, so when editMode goes from true to false, it will set the item's selected state.
Add this to your <Child /> component:
useEffect(() => {
if (!editMode) {
setSelected(false);
}
}, [editMode]);
If you use React.memo you can cache the Child components and prevent their re-renders.
const Parent = () => {
const [editMode, setEditMode] = useState(false);
const [childrenList, setChildrenList] = useState(INITIAL_LIST);
const [selected, setSelected] = useState([]);
const toggleEditMode = useCallback(() => {
if (editMode) {
setSelected([]);
}
setEditMode(!editMode);
}, [editMode]);
const deleteSelectedChildren = () => {
setChildrenList(childrenList.filter((x) => !selected.includes(x.id)));
setEditMode(false);
};
const onSelect = useCallback((id) => {
setSelected((prev) => {
if (prev.includes(id)) {
return prev.filter((x) => x !== id);
}
return [...prev, id];
});
}, []);
// Check when <Parent /> is re-rendered
console.log("Parent");
return (
<>
<h1>Long press an element to enable "Edit Mode"</h1>
<ul className="childrenWrapper">
{childrenList.map((content, index) => (
<Child
key={content.id}
index={index}
content={content}
editMode={editMode}
toggleEditMode={toggleEditMode}
onSelect={onSelect}
selected={selected.includes(content.id)}
/>
))}
</ul>
{editMode && (
<button onClick={deleteSelectedChildren}>DELETE SELECTED</button>
)}
</>
);
};
You have to wrap the functions you pass as props inside useCallback, otherwise they will be different on every Parent render, invalidating the memoization.
import { useRef, memo } from "react";
const Child = memo(
({ content, editMode, toggleEditMode, onSelect, selected }) => {
// Prevent re-rendering when setting timer thread
const timerRef = useRef();
// Toggle selection of the <Child /> and update selectedChildrenIndexes
const toggleCheckbox = () => {
if (!editMode) return;
onSelect(content.id);
};
// Toggle Edit mode after .5s of holding press on a Child component
const longPressStartHandler = () => {
timerRef.current = setTimeout(toggleEditMode, 500);
};
// Release setTimeout thread in case it's pressed less than .5s
const longPressReleaseHandler = () => {
clearTimeout(timerRef.current);
};
// Check when <Child /> is re-rendered
console.log("Child - " + content.id);
return (
<li
className={`childContent ${editMode && "editMode"} ${
selected && "selected"
}`}
onMouseDown={longPressStartHandler}
onMouseUp={longPressReleaseHandler}
onClick={toggleCheckbox}
>
<pre>
<code>{JSON.stringify(content)}</code>
</pre>
{editMode && (
<input type="checkbox" onChange={toggleCheckbox} checked={selected} />
)}
</li>
);
}
);
You can see a working example here.

Is it possible to partially apply a React component?

Say I have a <Button> component which takes two properties: text and id e.g.,
<Button text="delete" id="123"/>
Now say I have a list of user ids: [101, 102, 103, …]
Would it be possible to partially apply <Button>? e.g.,
ids.map(<Button text="delete" id={__}>)
Where __ is just a placeholder waiting to be replaced with the current id.
If it was possible, would partially applying a React component have any adverse effect on the React Reconciliation Algorithm?
You could use two ways
one, which is not really a partial
ids.map((id)=><Button text="delete" id={id} />)
and the partial one which is really extracting the function above and using it
const PartialDeleteButton = (id) => <Button text="delete" id={id} />
ids.map(PartialDeleteButton)
which you could also use as
<PartialDeleteButton id={5} />
i cannot see how these would affect the reconciliation algorithm
There is no partial render of a component in React.
A component watches on state and props. Whenever you change either one, it will refresh the component. So if you change id dynamically, it will re-render the component.
However that would be 1 extra re-render.
You can however choose to write functions to prevent that like
React.memo: For latest react
shouldComponentUpdate: For older version.
Following is a demo for React.memo:
What to look in fiddle, notice I have added a setTimeout that updates data and it will call the render of ToDoApp but since components are memoised, it will not be called
function Button({id, value}) {
const onClick = () => {
console.log(`${id} - ${value}`)
}
console.log(`Rendering Btn ${value}`)
return (<button id={id || '__'} onClick={onClick}>{ value }</button>);
}
const MyButton = React.memo(
Button,
(prevProps, nextProps) => {
return prevProps.value === nextProps.value
}
)
Note: Since you will stop rendering of a component, you will not get updated props in it.
You could use useCallback to get a similar effect to partial application:
const HelloGreeter = useCallback(({name}: {name: string}) =>
(<Greeter name={name} greet="hello" />), []);
So, in context:
interface GreeterProps {
greet: string
name: string
}
const Greeter = ({greet, name}: GreeterProps) => (
<div>{greet}, {name}</div>
);
const MyComponent = () => {
const [name1, setName1] = useState<string>("world")
const HelloGreeter = useCallback(({name}: {name: string}) =>
(<Greeter name={name} greet="hello" />), []);
const setNameCallback = useCallback((e: ChangeEvent<HTMLInputElement>) =>
setName1(e.target.value), []);
return(
<>
<HelloGreeter name={name1} >
<input value={name1} onChange={setNameCallback} />
</>
);
}
This would not confuse the React renderer, because useCallback defines the function once only.

prevent child component to re-render below context provider with memo

I am using a context provider in React to share data across several components. However since a value gets changed from one of my subcomponents, it rerenders all of my other components which partly leads to performance issues. So I want to prevent my child components to rerender. I tried using React.memo() but it's still rendering whenever I set the state of the Context Provider.
const Authenticator = React.memo(() => {
const [myChat, setMyChat] = useContext(ChatContext);
console.log("rerender"); // gets called everytime on click
return (
<Button
title="click me"
onPress={() => setMyChat({ text: "hello" })}
></Button>
);
});
My Context Provider looks like this:
const ChatProvider = ({ children }) => {
const [myChat, setMyChat] = useState([]);
return (
<ChatContext.Provider value={[myChat, setMyChat]}>
{children}
</ChatContext.Provider>
);
};
My App.js looks like this:
<ChatProvider>
<Authenticator />
</ChatProvider>
React.Memo doesn't help since you are calling the useContext hook which will cause the component to re-render every time the value from the provider changes. You should consider splitting your context into two separate contexts: one for the value, one for the state updater.
const ChatProvider = ({ children }) => {
const [myChat, setMyChat] = useState([])
return (
<ChatDispatchContext.Provider value={setMyChat}>
<ChatValueContext.Provider value={myChat}>
{children}
</ChatValueContext.Provider>
</ChatDispatchContext.Provider>
)
}
Then, update your Authenticator component to the following:
const Authenticator = React.memo(() => {
const setMyChat = useContext(ChatDispatchContext)
return (
<Button
title="click me"
onPress={() => setMyChat({ text: "hello" })}
></Button>
)
})

Accessing Apollo's loading boolean outside of Mutation component

The Mutation component in react-apollo exposes a handy loading boolean in the render prop function which is ideal for adding loaders to the UI whilst a request is being made. In the example below my Button component calls the createPlan function when clicked which initiates a GraphQL mutation. Whilst this is happening a spinner appears on the button courtesy of the loading prop.
<Mutation mutation={CREATE_PLAN}>
{(createPlan, { loading }) => (
<Button
onClick={() => createPlan({ variables: { input: {} } })}
loading={loading}
>
Save
</Button>
)}
</Mutation>
The issue I have is that other aspects of my UI also need to change based on this loading boolean. I have tried lifting the Mutation component up the React tree so that I can manually pass the loading prop down to any components which rely on it, which works, but the page I am building has multiple mutations that can take place at any given time (such as deleting a plan, adding a single item in a plan, deleting a single item in a plan etc.) and having all of these Mutation components sitting at the page-level component feels very messy.
Is there a way that I can access the loading property outside of this Mutation component? If not, what is the best way to handle this problem? I have read that you can manually update the Apollo local state using the update function on the Mutation component (see example below) but I haven't been able to work out how to access the loading value here (plus it feels like accessing the loading property of a specific mutation without having to manually write it to the cache yourself would be a common request).
<Mutation
mutation={CREATE_PLAN}
update={cache => {
cache.writeData({
data: {
createPlanLoading: `I DON"T HAVE ACCESS TO THE LOADING BOOLEAN HERE`,
},
});
}}
>
{(createPlan, { loading }) => (
<Button
onClick={() => createPlan({ variables: { input: {} } })}
loading={loading}
>
Save
</Button>
)}
</Mutation>
I face the same problem in my projects and yes, putting all mutations components at the page-level component is very messy. The best way I found to handle this is by creating React states. For instance:
const [createPlanLoading, setCreatePLanLoading] = React.useState(false);
...
<Mutation mutation={CREATE_PLAN} onCompleted={() => setCreatePLanLoading(false)}>
{(createPlan, { loading }) => (
<Button
onClick={() => {
createPlan({ variables: { input: {} } });
setCreatePLanLoading(true);
}
loading={loading}
>
Save
</Button>
)}
</Mutation>
I like the answer with React States. However, when there are many different children it looks messy with so many variables.
I've made a bit update for it for these cases:
const Parent = () => {
const [loadingChilds, setLoading] = useState({});
// check if at least one child item is loading, then show spinner
const loading = Object.values(loadingChilds).reduce((t, value) => t || value, false);
return (
<div>
{loading ? (
<CircularProgress />
) : null}
<Child1 setLoading={setLoading}/>
<Child2 setLoading={setLoading}/>
</div>
);
};
const Child1 = ({ setLoading }) => {
const [send, { loading }] = useMutation(MUTATION_NAME);
useEffect(() => {
// add info about state to the state object if it's changed
setLoading((prev) => (prev.Child1 !== loading ? { ...prev, Child1: loading } : prev));
});
const someActionHandler = (variables) => {
send({ variables});
};
return (
<div>
Child 1 Content
</div>
);
};
const Child2 = ({ setLoading }) => {
const [send, { loading }] = useMutation(MUTATION_NAME2);
useEffect(() => {
// add info about state to the state object if it's changed
setLoading((prev) => (prev.Child2 !== loading ? { ...prev, Child2: loading } : prev));
});
const someActionHandler = (variables) => {
send({ variables});
};
return (
<div>
Child 2 Content
</div>
);
};

mapStateToProps value lags behind getState() by a keystroke

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.

Resources