React Component Function Comparison - reactjs

I have a form that has gotten really intense. Roughly 20 inputs that include multiple datepickers, google locations api, rrule values etc. Since very few of these inputs can update directly without going through some sort of a transform. I've successfully converted the component to from a stateful component that was doing way too much in the lifecycle methods (now using formik to manage values), but I'm trying to determine what the best way to define the necessary helper functions (e.g. updatedDateWithTime, formatAddress) in terms of performance and style, and can only think of a few options.
Option one: function expressions within the functional component:
const MyHugeForm = () => {
const helper1 = () => { console.log("thing1") }
const helper2 = () => {console.log("thing2") }
return() {...}
}
Option 2: as "globals" defined in the file, outside of the function:
helper1() => console.log("thing1");
helper2() => console.log("thing2");
const MyHugeForm = () => {
return() {...}
}
Option 3: as inline arrow functions used inside child components (i.e. break each input into it's own component and pass props down)
const MyHugeForm = (props) => {
return() {
<div>
<DateInput startDate={props.startDate} />
<LocationInput location={props.googleLocation} />
</div>
}
}
const DateInput = (props) => {
<DatePicker onChange={() => console.log("thing1")} />
}
const LocationInput = (props) => {
<input onChange={() => console.log("thing2")} />
}
It feels wrong to define 20 or so of these helper functions outside of (but in the same file as) the functional component, but defining them as function expressions inside the component seems worse and of the two options worse for performance. Breaking the pieces into child components feels like the right pattern in terms of reducing the complexity of a 600 line functional component, but if the children just end up defining the same functions inline in their renders, isn't it effectively the same?

My suggestion would be to create a helper class with some static methods where you can pass the input html events as parameters:
export default class MyHugeFormHelper {
static onChangeHandler(e) {
// do stuff here
}
static onInputHandler(e) {}
static onSubmit(e, callback) {
// you could pass a callback function from the logic of your component
}
}
Then in your component invoke this class method like this:
import MyHugeFormHelper from './MyHugeFormHelper';
const DateInput = (props) => {
<DatePicker onChange={MyHugeFormHelper.onChangeHandler} />
}

Related

Create helper function for a (click handler) function to reuse in multiple React components

For a 'back' button I've created below (onClick) handler function in my React app.
const { length: historyLength, goBack, replace } = useHistory();
const handleBack = () => {
if (historyLength > 2) {
goBack();
} else {
// History length is 2 by default when nothing is pushed to history yet
// https://stackoverflow.com/questions/9564041/why-history-length-is-2-for-the-first-page
replace(HomePage);
}
};
Then I am passing the onClick handler to my child component like: <Button onClick={handleBack}/>
I am using this handleBack function in multiple places in my React app. Is it a good approach make it e.g. a helper function and how exactly?
I also don't see any issue with the code or using it as a utility callback.
Is it a good approach make it e.g. a helper function and how exactly?
Anytime you can make your code more DRY (Don't Repeat Yourself) it's generally a good thing. My personal rule-of-thumb is if I've written the same utility code a third time I'll spend a bit of time to refactor it into a common utility (and unit test!!).
I might suggest creating a custom hook to return the back handler.
Example:
import { useHistory } from 'react-router-dom';
const useBackHandler = () => {
const history = useHistory();
const handleBack = React.useCallback(() => {
const { length: historyLength, goBack, replace } = history;
if (historyLength > 2) {
goBack();
} else {
replace(HomePage);
}
}, []);
return handleBack;
};
export default useBackHandler;
Now you have a single hook to import and use.
import useBackHandler from '../path/to/useBackHandler';
...
const backHandler = useBackHandler();
...
<button type="button" onClick={backHandler}>Back?</button>
If you are needing this function in older class components, then you'll need a way to inject the handleBack as a prop. For this you can create a Higher Order Component.
Example:
import useBackHandler from '../path/to/useBackHandler';
const withBackHandler = Component => props => {
const backHandler = useBackHandler();
return <Component {...props} backHandler={backHandler} />;
};
export default withBackHandler;
To use, import withBackHandler and decorate a React component and access props.backHandler.
import withBackHandler from '../path/to/withBackHandler';
class MyComponent extends React.Component {
...
someFunction = () => {
...
this.props.backHandler();
}
...
}
export default withBackHandler(MyComponent);
#meez
Don't see why this wouldn't work. Just a couple of things: (a) I would add the event argument and e.preventDefault() within the function and (b) would be careful of the function name you are passing on the onClick property of your button: handleBackClick !== handleBack, you'll get an ReferenceError because of an undefined function.
Additionally, I also noticed that this can be achieved with native browser functions. Here's a snippet:
const { length: historyLength, back } = window.history;
const { replace } = window.location;
const handleBack = (e) => {
e.preventDefault();
if (historyLength > 2) {
back();
} else {
replace('homepageUrl');
}
};

How should I update individual items' className onClick in a list in a React functional component?

I'm new to React and I'm stuck trying to get this onClick function to work properly.
I have a component "Row" that contains a dynamic list of divs that it gets from a function and returns them:
export function Row({parentState, setParentState}) {
let divList = getDivList(parentState, setParentState);
return (
<div>
{divList}
</div>
)
}
Say parentState could just be:
[["Name", "info"],
["Name2", "info2"]]
The function returns a list of divs, each with their own className determined based on data in the parentState. Each one needs to be able to update its own info in parentState with an onClick function, which must in turn update the className so that the appearance of the div can change. My code so far seems to update the parentState properly (React Devtools shows the changes, at least when I navigate away from the component and then navigate back, for some reason), but won't update the className until a later event. Right now it looks like this:
export function getDivList(parentState, setParentState) {
//parentState is an array of two-element arrays
const divList = parentState.map((ele, i) => {
let divClass = "class" + ele[1];
return (
<div
key={ele, i}
className={divClass}
onClick={() => {
let newParentState =
JSON.parse(JSON.stringify(parentState);
newParentState[i][1] = "newInfo";
setParentState(newParentState);}}>
{ele[0]}
</div>
)
}
return divList;
}
I have tried to use useEffect, probably wrong, but no luck. How should I do this?
Since your Row component has parentState as a prop, I assume it is a direct child of this parent component that contains parentState. You are trying to access getDivList in Row component without passing it as a prop, it won't work if you write your code this way.
You could use the children prop provided by React that allow you to write a component with an opening and closing tag: <Component>...</Component>. Everything inside will be in the children. For your code it would looks like this :
import React from 'react';
import { render } from 'react-dom';
import './style.css';
const App = () => {
const [parentState, setParentState] = React.useState([
['I am a div', 'bg-red'],
['I am another div', 'bg-red'],
]);
React.useEffect(
() => console.log('render on ParentState changes'),
[parentState]
);
const getDivList = () => {
return parentState.map((ele, i) => {
return (
<div
key={(ele, i)}
className={ele[1]}
onClick={() => {
// Copy of your state with the spread operator (...)
let newParentState = [...parentState];
// We don't know the new value here, I just invented it for the example
newParentState[i][1] = [newParentState[i][1], 'bg-blue'];
setParentState(newParentState);
}}
>
{ele[0]}
</div>
);
});
};
return <Row>{getDivList()}</Row>;
};
const Row = ({ children }) => {
return <>{children}</>;
};
render(<App />, document.getElementById('root'));
And a bit of css for the example :
.bg-red {
background-color: darkred;
color: white;
}
.bg-blue {
background-color:aliceblue;
}
Here is a repro on StackBlitz so you can play with it.
I assumed the shape of the parentState, yu will have to adapt by your needs but it should be something like that.
Now, if your data needs to be shared across multiple components, I highly recommand using a context. Here is my answer to another post where you'll find a simple example on how to implement a context Api.

Expose state and method of child Component in parent with React

I know it's not a good pattern to do that, but you will understand why I want to do like that.
I have a HTable, which use a third-party library (react-table)
const HTable = <T extends object>({ columns, data, tableInstance}: Props<T>) {
const instance: TableInstance<T> = useTable<T> (
// Parameters
)
React.useImperativeHandle(tableInstance, () => instance);
}
Now, I want to control columns visibility from parent. I did:
const Parent = () => {
const [tableInstance, setTableInstance] = React.useState<TableInstance<SaleItem>>();
<Table data={data} columns={columns} tableInstance={(instance) => setTableInstance(instance)}
return tableInstance.columns.map((column) => {
<Toggle active={column.isVisible} onClick={() =>column.toggleHiden()}
}
}
The column hides well, but the state doesn't update and neither does the toggle, and I don't understand why. Could you help me to understand?
EDIT:
Adding a sandbox.
https://codesandbox.io/s/react-table-imperative-ref-forked-dilx3?file=/src/App.js
Please note that I cannot use React.forwardRef, because I use typescript and React.forwardRef doesn't allow generic type like this if I use forwardRef
interface TableProps<T extends object> {
data: T[],
columns: Column<T>[],
tableInstance?: React.RefObject<TableInstance<T>>,
}
Your issue is that react-tables useTable() hook always returns the same object as instance wrapper (the ref never changes). So your parent, is re-setting tableInstance to the same object - which does not trigger an update. Actually most of the contained values are also memoized. To get it reactive grab the headerGroups property.
const {
headerGroups,
...otherProperties,
} = instance;
React.useImperativeHandle(
tableInstance,
() => ({ ...properties }), // select properties individually
[headerGroups, ...properties],
);

Create Dynamic Components

I want to dynamically create a component, when I implement something like this:
const gen_Comp = (my_spec) => (props) => {
return <h1>{my_spec} {props.txt}</h1>;
}
const App = () => {
const Comp = gen_Comp("Hello");
return (
<Comp txt="World" />
);
}
Something goes wrong (what exactly goes wrong is hard to explain because it's specific to my app, point is that I must be doing something wrong, because I seem to be losing state as my component gets rerendered). I also tried this with React.createElement, but the problem remains.
So, what is the proper way to create components at runtime?
The main way that react tells whether it needs to mount/unmount components is by the component type (the second way is keys). Every time App renders, you call gen_Comp and create a new type of component. It may have the same functionality as the previous one, but it's a new component and so react is forced to unmount the instance of the old component type and mount one of the new type.
You need to create your component types just once. If you can, i recommend you use your factory outside of rendering, so it runs just when the module loads:
const gen_Comp = (my_spec) => (props) => {
return <h1>{my_spec} {props.txt}</h1>;
}
const Comp = gen_Comp("Hello");
const App = () => {
return (
<Comp txt="World" />
);
}
If it absolutely needs to be done inside the rendering of a component (say, it depends on props), then you will need to memoize it:
const gen_Comp = (my_spec) => (props) => {
return <h1>{my_spec} {props.txt}</h1>;
}
const App = ({ spec }) => {
const Comp = useMemo(() => {
return gen_Comp(spec);
}, [spec]);
return (
<Comp txt="World" />
);
}

React how to test props passed to component passed as prop

Okay, this might sound complicated but it will be easy if you read the following example. The main purpose of this is to separate the logic from the actual render code. Making the component smaller and (in theory) easier to test.
class NameProvider {
public getName(): Promise<string> {
return Promise.resolve("Cool name");
}
}
interface RenderProps {
name: string;
onGetNamePress(): void;
}
interface LogicProps {
nameProvider: NameProvider;
render: React.ComponentType<RenderProps>
}
function Render({name, onGetNamePress}: RenderProps): React.ReactElement {
return <>
<p>{name}</p>
<button title="Get name!" onClick={onGetNamePress} />
</>
}
function Logic({nameProvider, render: Render}: LogicProps): React.ReactElement {
const [name, setName] = React.useState<string>();
return <Render
name={name}
onGetNamePress={fetch}
/>
async function fetch() {
setName(await nameProvider.getName());
}
}
Testing the render component is rather easy, but how do I test that the props passed to the render component are correct? Especially after the state changed.
Consider the following:
it('fetches the name after the button was pressed', () => {
const mnp = new MockNameProvider();
render(<Logic
nameProvider={mnp}
render={({name, onGetNamePress}) => {
act(async () => {
await onGetNamePress();
expect(name).toBe(mockName);
})
}}
/>)
})
This will cause an infinite loop, as the state keeps getting changed and the name fetched. I also couldn't imagine how to get the new props. This current code will test the old ones to my understanding. So my question is, how do I test if the props are correctly passed (also after updates).
(Important) Notes:
I'm actually writing a react native app, so maybe the issue is specific to native testing but I didn't think so.
This is not code from our codebase and just cobbled together. Thus also the React prefix, vscode just liked that better in an unsaved file.

Resources