Handle a custom component using React Hook Forms and React Table - reactjs

I'm trying to set up a custom component to my react-hook-forms and react-table
const selectedUserIds = watch(`user_ids`) || [];
<Controller
control={control}
name={fieldName("user_ids")}
render={({ field: { value, onChange } }) => (
<UserTable selectedUserIds={selectedUserIds} updatePropertySelection={onChange}/>
)}
/>
UserTable:
function updatePropertyTableSelection(ids){
const ids = extractIds(ids);
updatePropertySelection(ids);
}
const selectedUserIdsForTable = convertIdsToTableFormat(selectedUserIds)
return (
<React.Fragment>
<BasicTable
tableHeader={() => null}
columns={[
{
Header: "User Name",
accessor: "attributes.name",
}
]}
data={data}
selectedRows={selectedUserIdsForTable}
onSelectedRowsChange={updatePropertyTableSelection}
/>
</React.Fragment>
)
When I associate the onChange handler to updatePropertySelection I get stuck in an infinite rerender. How should I prevent that?

I faced the same issue and I fix it by following this:
First one, my issue comes from the old react-hook-form, now I'm using ^7.14.2 and its resolve issue.
Second solution fixed issue by replacing onChange to field.onChange
for example (from real project):
<Controller
name={"explanation"}
control={props.control}
render={({field}) => {
return (
<MyEditor
placeholder={t("onlineAssessments.question.explanationPlaceholder")}
onPressEnter={props.onPressEnter}
onChange={(val, editor) => {
// notify parent
field.onChange(val);
// calc count
setExplanationCount(editor?.getBody().innerText.trim().length)
}
}
/>
)
}
}
/>
and I think this change comes from the latest update..
May you have something keep re-render trigger, base on your table, you may need to useCallBack or useMemo to prevent re-render on each visit else if dependency change, for example:
const updatePropertyTableSelection = useCallBack((ids) => {
const ids = extractIds(ids);
updatePropertySelection(ids);
}, []);
and
const selectedUserIdsForTable = useMemp(() => { convertIdsToTableFormat(selectedUserIds)
}, []);
Note: Don't forget to update dependency only if it's needed and only to what you needed to re-render

Related

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.

Incorrect use of useEffect() when filtering an array

I have this React app that's is getting data from a file showing in cards. I have an input to filter the cards to show. The problem I have is that after I filter once, then it doesn't go back to all the cards. I guess that I'm using useEffect wrong. How can I fix this?
import { data } from './data';
const SearchBox = ({ onSearchChange }) => {
return (
<div>
<input
type='search'
placeholder='search'
onChange={(e) => {
onSearchChange(e.target.value);
}}
/>
</div>
);
};
function App() {
const [cards, setCards] = useState(data);
const [searchField, setSearchField] = useState('');
useEffect(() => {
const filteredCards = cards.filter((card) => {
return card.name.toLowerCase().includes(searchField.toLowerCase());
});
setCards(filteredCards);
}, [searchField]);
return (
<div>
<SearchBox onSearchChange={setSearchField} />
<CardList cards={cards} />
</div>
);
}
you should Include both of your state "Card", "searchedField" as dependincies to useEffect method.once any change happens of anyone of them, your component will re-render to keep your data up to date,
useEffect(() => { // your code }, [searchField, cards]);
cards original state will be forever lost unless you filter over original data like const filteredCards = data.filter().
though, in a real project it's not interesting to modify your cards state based on your filter. instead you can remove useEffect and create a filter function wrapped at useCallback:
const filteredCards = useCallback(() => cards.filter(card => {
return card.name.toLowerCase().includes(searchField.toLowerCase());
}), [JSON.stringify(cards), searchField])
return (
<div>
<SearchBox onSearchChange={setSearchField} />
<CardList cards={filteredCards()} />
</div>
);
working example
about array as dependency (cards)
adding an object, or array as dependency at useEffect may crash your app (it will throw Maximum update depth exceeded). it will rerun useEffect forever since its object reference will change everytime. one approach to avoid that is to pass your dependency stringified [JSON.stringify(cards)]

How to test functionality of function props in storybook?

I have a parent component, <AssetSelectorMenu>, which is composed of two child components:
export const AssetSelectorMenu = (({ assets, sortByName }) => {
return (
<>
<AssetSelectorHeader sortByName={sortByName} />
{assets && assets.map((asset) => (
<AssetSelectorRow key={asset} />
))}
</>
);
});
storybook for AssetSelectorMenu:
export const Default = () => (
<AssetSelectorMenu sortByName={action("sortByName")} assets={assets} />
);
Inside the storybook for AssetSelectorMenu, I'd like to test that the function prop sortByName actually visually sorts the assets by name. At the moment, it only makes sure it the function gets called, but visually it's not sorting the assets. How can I do that?
If you want to use state in your Storybook examples so that your components are fully working based on interaction you need to use the createElement function from React.
Here is a simple example using a Checkbox component that has it's value managed by state which simulates using a state manager like Redux or Context etc.
import { Fragment, useState, createElement } from 'react'
<Preview>
<Story name="Default">
{createElement(() => {
const [value, setValue] = useState(['Yes'])
const onChange = (event, value) => {
setValue(value)
}
return (
<Checkbox
name="checkbox"
values={[
{ label: 'Yes', value: 'Yes' },
{ label: 'No', value: 'No' }
]}
value={value}
onChange={onChange}
/>
)
})}
</Story>
</Preview>

How to render a different component with React Hooks

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.

Refactoring class component to functional component with hooks, getting Uncaught TypeError: func.apply is not a function

This is my first attempt to refactor code from a class component to a functional component using React hooks. The reason we're refactoring is that the component currently uses the soon-to-be-defunct componentWillReceiveProps lifecylcle method, and we haven't been able to make the other lifecycle methods work the way we want. For background, the original component had the aforementioned cWRP lifecycle method, a handleChange function, was using connect and mapStateToProps, and is linking to a repository of tableau dashboards via the tableau API. I am also breaking the component, which had four distinct features, into their own components. The code I'm having issues with is this:
const Parameter = (props) => {
let viz = useSelector(state => state.fetchDashboard);
const parameterSelect = useSelector(state => state.fetchParameter)
const parameterCurrent = useSelector(state => state.currentParameter)
const dispatch = useDispatch();
let parameterSelections = parameterCurrent;
useEffect(() => {
let keys1 = Object.keys(parameterCurrent);
if (
keys1.length > 0 //if parameters are available for a dashboard
) {
return ({
parameterSelections: parameterCurrent
});
}
}, [props.parameterCurrent])
const handleParameterChange = (event, valKey, index, key) => {
parameterCurrent[key] = event.target.value;
console.log(parameterCurrent[key]);
return (
prevState => ({
...prevState,
parameterSelections: parameterCurrent
}),
() => {
viz
.getWorkbook()
.changeParameterValueAsync(key, valKey)
.then(function () {
Swal.fire({
position: "center",
icon: "success",
title:
JSON.stringify(key) + " set to " + JSON.stringify(valKey),
font: "1em",
showConfirmButton: false,
timer: 2500,
heightAuto: false,
height: "20px"
});
})
.otherwise(function (err) {
alert(
Swal.fire({
position: "top-end",
icon: "error",
title: err,
showConfirmButton: false,
timer: 1500,
width: "16rem",
height: "5rem"
})
);
});
}
);
};
const classes = useStyles();
return (
<div>
{Object.keys(parameterSelect).map((key, index) => {
return (
<div>
<FormControl component="fieldset">
<FormLabel className={classes.label} component="legend">
{key}
</FormLabel>
{parameterSelect[key].map((valKey, valIndex) => {
console.log(parameterSelections[key])
return (
<RadioGroup
aria-label="parameter"
name="parameter"
value={parameterSelections[key]}
onChange={(e) => dispatch(
handleParameterChange(e, valKey, index, key)
)}
>
<FormControlLabel
className={classes.formControlparams}
value={valKey}
control={
<Radio
icon={
<RadioButtonUncheckedIcon fontSize="small" />
}
className={clsx(
classes.icon,
classes.checkedIcon
)}
/>
}
label={valKey}
/>
</RadioGroup>
);
})}
</FormControl>
<Divider className={classes.divider} />
</div>
);
})
}
</div >
)};
export default Parameter;
The classes const is defined separately, and all imports of reducers, etc. have been completed. parameterSelect in the code points to all available parameters, while parameterCurrent points to the default parameters chosen in the dashboard (i.e. what the viz initially loads with).
Two things are happening: 1. Everything loads fine on initial vizualization, and when I click on the Radio Button to change the parameter, I can see it update on the dashboard - however, it's not actually showing the radio button as being selected (it still shows whichever parameter the viz initialized with as being selected). 2. When I click outside of the Filterbar (where this component is imported to), I get Uncaught TypeError: func.apply is not a function. I refactored another component and didn't have this issue, and I can't seem to determine if I coded incorrectly in the useEffect hook, the handleParameterChange function, or somewhere in the return statement. Any help is greatly appreciated by this newbie!!!
This is a lot of code to take in without seeing the original class or having a code sandbox to load up. My initial thought is it might be your useEffect
In your refactored code, you tell your useEffect to only re-run when the props.parameterCurrent changes. However inside the useEffect you don't make use of props.parameterCurrent, you instead make use of parameterCurrent from the local lexical scope. General rule of thumb, any values used in the calculations inside a useEffect should be in the list of re-run dependencies.
useEffect(() => {
let keys1 = Object.keys(parameterCurrent);
if (
keys1.length > 0 //if parameters are available for a dashboard
) {
return ({
parameterSelections: parameterCurrent
});
}
}, [parameterCurrent])
However, this useEffect doesn't seem to do anything, so while its dependency list is incorrect, I don't think it'll solve the problem you are describing.
I would look at your dispatch and selector. Double check that the redux store is being updated as expected, and that the new value is making it from the change callback, to the store, and back down without being lost due to improper nesting, bad key names, etc...
I'd recommend posting a CodeSandbox.io link or the original class for further help debugging.

Resources