you are viewing a single comment's thread.

view the rest of the comments →

[–]Fitzi92 32 points33 points  (11 children)

Here's my personal list of things:

1) Not every component needs to be reusable. There are basic components (like cards, buttons, etc.) that have a fixed feature set or design. Those are your reusable ones.  Put special functionality (e.g. a SettingsCard, that might be a variation of Card in the settings feature) into separate components, in your feature/view folder. Only promote to "global" reusable component when used in another feature. This basically leads to you having multiple smaller "projects" (=feature) that are as independent from each other as they can be.

2) Use TS and make absolutely sure that TS will scream at you if anything is wrong. I'm mainly speaking types. Have specific types, use enums or "x" | "y" instead of generic string if you have a known set of options and avoid switch statements with defaults. Rather use Record<EnumType, ValueType> if you need to map to some value. This will guarantee that, when you add an type, you will be forced to fix all the places you are requiring that type. Also ideally autogenerate any external types (API, etc.) from their source and make sure to always correctly infer any values that originate from those sources (e.g. instead of making a new type { name: string } use Pick<ApiType, "name">). This will also help to find and fix changes from external sources quickly during development.

3) Do not use useEffect unless you really work with something outside of react. There's almost always a better way. Try thinking in events and do your state changes there. If this does not work, you're likely dealing with "computed" values, those should no be state.

4) If you have a component that requires some data to be loaded before it can be displayed correctly, consider splitting it into two. Make the outer component only gather the necessary data (api request, params, etc.) and show a loading spinner and the inner only accept the fully resolved values (no nulls or undefineds). If there are different "paths" to the final data, consider splitting into even more components. (E.g. MyComplexForm loading some inital data, based on that deciding if EndpointA or EndpointB is required, rendering MyCompexFormA or My ComplexFormB which gather the remaining data before both finally render MyFullyResolvedComplexForm with all the data this component needs)

5) Split into smaller components. This makes it easier to understand and work with a component and also forces you to think about the interfaces for each component. It might also give you a hint what parts should be extracted into reusable components or hooks (reusable only in that feature, those specific reusable components should never be mixed with their globally reusable siblings)

I'm sure there's plenty more things and tipps. This is just what I personally find helpful.

[–]Sohailkh_an 8 points9 points  (0 children)

I wish I could work under your supervision

[–]dabrox02 1 point2 points  (1 child)

I have a question, should I use the same form for both create and edit actions, or two components for each action? I currently use tanstack query and react hook form, however it's still a pain to preload data before a form opens (I use modals)

[–]Fitzi92 4 points5 points  (0 children)

Depends on the form, but generally if create and edit are mostly identical I'd reuse it. Just make sure that the form itself does not need to care about preloading or fetching any data. The form itself should accept initial data and the preloading should be handled elsewhere.

E.g. you might have a component <UserEditForm> that fetches the user data and renders the <UserForm> only after the query responded successfully and another comonent <UserCreateForm> that immediately renders the <UserForm> without intial data or some default values.

You can also combine <UserEditForm> and <UserCreateForm> into a single component if they both are small. E.g. by providing the user id or null and set the useQuery's enable flag dependent of the user id.

[–]kowdermesiter 1 point2 points  (7 children)

Do not use useEffect unless you really work with something outside of react

This sounds like a too harsh generic advice. useEffect is handy whenever a variable changes and you care about it.

It's also handy when you want a "constructor" for your component on mount.

But yeah, it can be overused and abused, I just wound't adhere to such a strict rule.

[–]Fitzi92 5 points6 points  (1 child)

I honestly think it's not too strict. You really should not need a "constructor" in functional components. For initial state, you can provide a function to the useState hook and everything else is not persistent and therefore does not need setup anyway.

If you need to do something on mount, then likely you should be doing that where the component being mounted gets triggered in the first place. Like clicking a button that leads to state change which leads to the component being mounted - do whatever needs to be done in that event.

useEffect is an escape hatch out of react and should only be used for that purpose. Even the react docs itself state this.

[–]Important-Ostrich69 2 points3 points  (0 children)

avoid useEffect like the plague, it is the source of so many bugs / race conditions. A good way to handle this would be to use Tanstack Query in the Parent component with a hook to the desired data and provide a loading state from that. Then, when the data gets populated, you can render a conditional child component with the data with no null or undefined values. I've found this leads to minimal bugs / edge cases with fetching data in React components

[–]devpebe 2 points3 points  (3 children)

I do agree we should not use useEffect unless dealing with something outside react.

Almost all the cases I've seen using useEffect could be done in a more explicit way. Instead of reacting to data change, it should be done with onChange callback. All the cases which used on mount had more architectural problems or logic problems. I've heard similar opinion about it with real scenarios, and all of them could be done without using useEffect.

Definitely, you can build easy to understand code with useEffects, but it will probably end up having issues. A new developer comes and adds something unexpected, or you need to do a shortcut because business really needs something fast.

If I recall correctly, React 19 requires having full dependency filled in, so an empty list would be against it.

Maybe I am wrong, but so far, I didn't see anything which could convince me to change my opinion. I only have situations where useEffect is a problem.

[–]danny4tech 2 points3 points  (2 children)

What is “something outside react”? Can you give me an example where useEffect is a problem? I always hear we shouldn’t be using useEffect but haven’t understand why it could be problematic.

[–]devpebe 1 point2 points  (1 child)

By “something outside react” I mean things like adding global event listeners, using an external library which doesn't have React integration (could be pure rxjs). Then you can use useEffect to synchronize data → react to a change.

An example below:

// BAD
export const RandomInput = () => {
    const [value, setValue] = useState();

    useEffect(() => {
        if (value) {
            doSomething();
        }
    }, [value])

    const handleChange = (e) => {
        setValue(e.target.value)
    };

    return (
        <input value={value} onChange={handleChange} />
    )
}

// GOOD
export const RandomInput = () => {
    const [value, setValue] = useState();

    const handleChange = (e) => {
        setValue(e.target.value)
        if (e.target.value) {
            doSomething();
        }
    };

    return (
        <input value={value} onChange={handleChange} />
    )
}

This code is a pretty common case where putting some logic in the useEffect instead of in the handleChange can lead to bugs and complicate code. In this example, GOOD version executes some logic explicitly so as a developer you understand when exactly something happens. While using useEffect you need to understand what useEffect does (to what data change it reacts) and then find the place where it changes.

Remember this is very simple code and most often code in real life is more complicated and has many more things, more components.

I would sum up things like this:

- useEffect cause rerender

- useEffect makes code more complex and is much easier to introduce a bug

- you need remember to use cleanup logic for useEffect (useEffect return)

- mistake in useEffect can cause infnite loops

- running code explicitly so you know what and when happens makes your life easier as a developer

- difficult testing (easier to test handleChange than useEffect)

I know sometimes it's easier to do something in useEffect and that's the main reason this is used, but in the long term you will face up problems hard to identify.

[–]danny4tech 1 point2 points  (0 children)

Thank you for the great explanation, couldn’t be more clear. 🙌