purescript-react / purescript-react-basic-hooks

An implementation of React hooks on top of purescript-react-basic
https://pursuit.purescript.org/packages/purescript-react-basic-hooks/
Apache License 2.0
200 stars 33 forks source link

Recursive React components & un-mounting/ re-rendering of components #69

Closed bentongxyz closed 2 years ago

bentongxyz commented 2 years ago

Hi there, when I try to create recursive react components, e.g. a folder structure UI:

RecursiveFolderComponent
├── RecursiveFolderComponent
│   ├── RecursiveFolderComponent
│   ├── RecursiveFolderComponent
├── RecursiveFolderComponent
    ├── RecursiveFolderComponent
    │   ├── RecursiveFolderComponent
    ├── RecursiveFolderComponent

I am hit with problems of all children components un-mounting and re-rendering (and React.memo also fails to work) when the parent component's state is updated.

I can make it work by using impure unsafePerformEffect like so:

recursiveFolder ∷ ReactComponent { folderId ∷ Int }
recursiveFolder = unsafePerformEffect $ memo' eq $
--                      ^ If I do not use `unsafePerformEffect`, 
--                        subfolder components will keep unmount and re-render
  reactComponent "RecursiveComponent" \props -> React.do
    ... 
    let
      arrOfSubFolders = const (element recursiveFolder { folderId: props.folderId - 1 })
        <$> (mkRows props.folderId)
    pure $ ...
          , React.fragment arrOfSubFolders
     ...

I have made a minimal reproducible code at:

If I do not use unsafePerformEffect, the children sub components will always un-mount and re-render, and I have to use unsafeRenderEffect in the middle of the component anyway (see below):

I think the issue is (to quote your succinct comment):

React uses function instances as component identity. ... Creating component functions during render results in forced unmouting and remounting of the entire tree below that component...

Originally posted by @megamaddu in https://github.com/megamaddu/purescript-react-basic-hooks/issues/12#issuecomment-573794368

Is there no other way to achieve memoization of recursive children components in Purescript without using unsafePerformEffect/ unsafeRenderEffect that might have arbitrary side-effect?

Is this one of the cases that Javascript/React refuses to play nicely with pure functional approach?

Thank you for reading through this long-winded issue.

Lastly, this is a Codepen | Typescript Implementation of how I would write it in "normal" Typescript, for refererence.

megamaddu commented 2 years ago

The comment you linked is the answer to this question.

When you log to the console, it has the type Effect Unit because there's a side effect. The side effect is a change to the world outside the program, where the number of times you run the effect and the way you order those effects matters. All programs have some kind of effect, or there'd be no reason to run them. PureScript just tells you when/where they're happening using the Effect a type.

So why is component creation side-effecting? Because each function instance or class declaration is a separate component, even if they have the same name and were created from the same code. And it is up to you as the creator of the component to decide when that effect happens. In JS/TS it's at the module level, by convention. If you wrapped your TS component in a function and called it multiple times you'd see the same re-mounting behavior. If you wrapped your TS component in a function and immediately invoked it (const RecursiveFolder = (() => ...your existing implementation...)();), you'd have written it exactly the way you've written it in the Try PureScript example. Using unsafePerformEffect at the module level is the way TS/JS do it, it's just implicit because side-effecting anywhere/everywhere is the status quo in TS/JS.

The more "PureScript way" would be to keep your component definition as an Effect and invoke that effect at the proper time in main, before you call render.

bentongxyz commented 2 years ago

Because each function instance or class declaration is a separate component, even if they have the same name and were created from the same code. And it is up to you as the creator of the component to decide when that effect happens. In JS/TS it's at the module level, by convention.

thank you that makes a lot of sense.

I don't mean to beat a dead horse, but what would be the "recommended way" to approach recursive React component?

If we behave and do the "Purescript way": invoking the effect of function instance creation in main, then the recursive component cannot directly reference itself (because that function instance has not been created yet).

So the way to do it will be to pass that instance that we created at main as a prop back to the recursive component?

I have re-implemented the RecursiveFolder component above using this approach Try Purescript | RecrusiveFolder as Props as an example.

megamaddu commented 2 years ago

The pattern I've used is to create dependent components as part of the effect for creating the parent component. I usually name components with a mk prefix, so it's clear how this effect/thunk behavior works when reading components structured this way. The Component alias makes this pattern easier as well.

mkApp :: Component Unit
mkApp = do
  sidebar <- mkSidebar
  content <- mkContent

  component "App" \_ -> React.do
    ...

mkSidebar :: Component Unit
mkSidebar = do
  component "Sidebar" \_ -> React.do
    ...

mkContent :: Component Unit
mkContent = do
  somethingElse <- mkSomethingElse

  component "Content" \_ -> React.do
    ...

Technically this does result in a few low level components being "duplicated", but if their parents are being re-mounted with new component types they're going to be re-mounted anyway.

bentongxyz commented 2 years ago

Thank you! really appreciate your help :) @megamaddu