Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

react-reconciler questions #13006

Closed
shichongrui opened this issue Jun 9, 2018 · 17 comments
Closed

react-reconciler questions #13006

shichongrui opened this issue Jun 9, 2018 · 17 comments

Comments

@shichongrui
Copy link

shichongrui commented Jun 9, 2018

Do you want to request a feature or report a bug?
No

I'm trying to build a custom renderer for react using react-reconciler. Most of the documentation and examples I've been able to find support rendering to a target once, but not re-rendering. But this is what I've been able to discover and am looking for more info on.

prepareUpdate allows you to generate a diff of the old props and new props. The examples I've found say this should return an array but I'm not sure what the items in this array should look like. So for now I'm just returning true from this method.

commitUpdate is run after prepareUpdate with old and new props, after which I can call the instance to tell it to update itself based on those props. But most examples I've found do not rely on the children from props and rather rely on children to come through methods such as appendChild/removeChild. It appears that when commitUpdate is run, none of the various *Child methods are called leading the newProps in commitUpdate to not reflect the children that are being tracked inside of the class. In this case I'm curious if perhaps I'm missing a *Child method in my implementation that is necessary but I don't know exists.

appendChild(child) {
  this.children.push(child)
}

commitUpdate (oldProps, newProps) {
  // this.children != newProps.children
}

I've also noticed that in order for commitUpdate to be called you must also pass in supportsMutation. Is this the correct property to get this working?

In some examples I see that projects also have a mutation object in their host config that includes essentially copies of the *Child and commitUpdate methods from the host config but when I try to do this in my renderer, those methods never seem to be called.

I'm also curious what the correct paradigm for attaching the underlying instances to one another. Currently, each of my elements has a render method that will call the render method of all of it's children and then add those children as sub views to the instance. But in cases where children might change, if I called render again I would end up with duplicate children in the parent. I've seen some examples where the elements attach themselves to their parents, rather than rendering all children and attaching the children to itself, which seems like it would resolve this issue, but this brings up another question.

For a custom element, when is the correct time to add/remove children from the underlying instance? Most examples I've seen use *Child methods to keep track of the children internally and then have a method that renders all children and adds them as sub views. But there is a question in my mind as to whether or not the *Child methods are where I should be adding the children as subviews to the instance. For example, when an instance's appendChild method is called, that's when I should add the subview as a child to the element. When removeChild is called, that is when I should remove that subview from the instance. Rather than simply using these methods to keep track of children to then render them at some other time.

appendChild(child) {
  this.view.addSubview(child)
}
removeChild(child) {
  this.view.removeSubview(child)
}

Rather than

children = new Set()
appendChild(child) {
  this.children.add(child)
}
removeChild(child) {
  this.children.delete(child)
}
render() {
  this.children.forEach(child => {
    this.view.addSubview(this.child.render())
  })
  return this.view
}
@gaearon
Copy link
Collaborator

gaearon commented Jun 9, 2018

prepareUpdate allows you to generate a diff of the old props and new props. The examples I've found say this should return an array but I'm not sure what the items in this array should look like. So for now I'm just returning true from this method.

It can return anything from prepareUpdate (we typically use arrays but it’s not required). Whatever you return will be passed as the second argument to commitUpdate.

The first method lets you calculate a “diff” between props, and the second one is where you apply it. The methods are separated so that diffing can happen asynchronously, but the actual mutation is only performed after everything is ready.

Typically we use a form like [propName1, propValue1, propName2, propValue2, ...] for props that changed. But you can use any other format.

commitUpdate is run after prepareUpdate with old and new props, after which I can call the instance to tell it to update itself based on those props. But most examples I've found do not rely on the children from props and rather rely on children to come through methods such as appendChild/removeChild.

This is right. To correctly “handle” children you’d need to reimplement React itself. So instead React does it for you, and calls the appropriate methods for adding, removing, and moving them.

It appears that when commitUpdate is run, none of the various *Child methods are called leading the newProps in commitUpdate to not reflect the children that are being tracked inside of the class.

I’m not 100% sure what you mean. A specific example would help. React should take care of calling append/insertBefore/removeChild when appropriate and you shouldn’t do anything special for that to work.

Maybe ReactDOM implementation would help? It’s here:

https://github.com/facebook/react/blob/master/packages/react-dom/src/client/ReactDOMHostConfig.js

I've also noticed that in order for commitUpdate to be called you must also pass in supportsMutation. Is this the correct property to get this working?

Yes.

In some examples I see that projects also have a mutation object in their host config that includes essentially copies of the *Child and commitUpdate methods from the host config but when I try to do this in my renderer, those methods never seem to be called.

Those examples use an older reconciler version. The new one removes the mutation object and instead uses supportsMutation property, and puts those methods in the top level object. So your approach was correct.

I'm also curious what the correct paradigm for attaching the underlying instances to one another. Currently, each of my elements has a render method that will call the render method of all of it's children and then add those children as sub views to the instance. But in cases where children might change, if I called render again I would end up with duplicate children in the parent. I've seen some examples where the elements attach themselves to their parents, rather than rendering all children and attaching the children to itself, which seems like it would resolve this issue, but this brings up another question.

I’m struggling to understand your description. You shouldn’t need to explicitly “manage” children of any nodes except implementing a few methods like appendChild. React will call them for you. You don’t need to attach anything or manage parent/child relationships yourself. Again, please refer to the DOM implementation above. Does this help?

For a custom element, when is the correct time to add/remove children from the underlying instance?

I’m also struggling to understand what you’re asking exactly here. But your first code example after this paragraph looks more reasonable than the second one. I don’t understand what the second one is doing (reconciler doesn’t call any method called “render”).

@shichongrui
Copy link
Author

shichongrui commented Jun 9, 2018

I’m not 100% sure what you mean. A specific example would help. React should take care of calling append/insertBefore/removeChild when appropriate and you shouldn’t do anything special for that to work.

I think your answers above answered that question for me.

I’m struggling to understand your description. You shouldn’t need to explicitly “manage” children of any nodes except implementing a few methods like appendChild. React will call them for you. You don’t need to attach anything or manage parent/child relationships yourself. Again, please refer to the DOM implementation above. Does this help?

In most of the examples I've seen, they keep track of their children inside of their custom elements. For example
react-synth
https://github.com/FormidableLabs/react-synth/blob/master/src/elements/osc.js#L85
Then they have a method that they call to render and attach children as subviews to the underlying instances.
https://github.com/FormidableLabs/react-synth/blob/master/src/elements/osc.js#L29

Or in proton-native
https://github.com/kusti8/proton-native/blob/master/src/components/DesktopComponent.js#L68
Then a method to render these children
https://github.com/kusti8/proton-native/blob/master/src/components/DesktopComponent.js#L149

Those methods are called in their own way, typically in some kind of way following the initial render.

I’m also struggling to understand what you’re asking exactly here. But your first code example after this paragraph looks more reasonable than the second one. I don’t understand what the second one is doing (reconciler doesn’t call any method called “render”).

The above examples are along the lines of what I'm referring to. They are using the appendChild/removeChild to keep track of children internally and then they have some other method that they use to actually render the underlying views for these children, (libui widgets in the case of proton-native). I'm curious if this is the correct approach or if in these cases, I should just be doing the work of attaching subviews/removing subviews from the underlying UI library when these methods are called.

Which brings up another question for me, is there a list of all of the methods react-reconciler or react will call to do all the operations of adding/removing children? This is the list I have been able to put together reading through other projects:

appendChildBeforeMount
appendChild
removeChild
insertBefore
insertInContainerBefore

But I'm not sure if this is it or if some of these are even correct.

@gaearon
Copy link
Collaborator

gaearon commented Jun 9, 2018

In most of the examples I've seen, they keep track of their children inside of their custom elements

Well, I guess it depends on whether there exists some API that manages underlying children or not.

If there is such an API (such as the case with DOM) then that’s what you should use. The whole premise of using the reconciler is it lets you translate declarative “render” operations into imperative platform-specific “append/insert/remove” calls. Doing the translation the other way around sounds like losing valuable information to me.

Overall I don’t recommend looking at those projects for the “intended” way to use these methods. Look at our own code. I already linked to the DOM implementation of these methods:

https://github.com/facebook/react/blob/master/packages/react-dom/src/client/ReactDOMHostConfig.js

Does it help?

I'm curious if this is the correct approach or if in these cases, I should just be doing the work of attaching subviews/removing subviews from the underlying UI library when these methods are called.

Calling the underlying library’s append/insert/remove methods is definitely the intended approach.

is there a list of all of the methods react-reconciler or react will call to do all the operations of adding/removing children

Yes, I linked above to the DOM renderer host config. Every export in it is a method you need to implement. There are several “sets” (common, mutation, persistence, hydration). You probably only need common+mutation.

You can also find the full list here:

https://github.com/facebook/react/blob/master/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js

@shichongrui
Copy link
Author

The pattern of keeping track of children in the custom element is also the suggested way to do it here https://github.com/nitin42/Making-a-custom-React-renderer/blob/master/part-two.md

@shichongrui
Copy link
Author

Awesome. I'll look more into the React DOM implementation. This has all been super helpful. I really appreciate your willingness to help me understand all this better.

@gaearon
Copy link
Collaborator

gaearon commented Jun 9, 2018

Cool. I updated the README of react-reconciler to remove that guide until it's fixed.
Also filed an issue. nitin42/Making-a-custom-React-renderer#10

I'm sorry but I didn't realize this guide offered confusing advice. Thanks for bringing this to my attention.

@thysultan
Copy link

@gaearon Does persistence mode expose a new method that is invoked or does it change the order/arguments with which the common methods are called, and If so what's the difference?

@gaearon
Copy link
Collaborator

gaearon commented Jun 10, 2018

Look at the whole list here:

https://github.com/facebook/react/blob/master/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js

Most renderers you know include common methods + mutation.

Persistent renderer includes common + persistence instead.

Either of them could optionally support hydration.

@nitin42
Copy link

nitin42 commented Jun 10, 2018

@shichongrui I've updated the tutorial (Making custom React renderer) and also the code for custom components. Let me know!

@shichongrui
Copy link
Author

Awesome! Thanks for the great tutorial! It’s the only one that I could find that is very detailed.

@ex3ndr
Copy link

ex3ndr commented Aug 21, 2018

Not sure if i should create new issue, but i have a question: is there a way to forward context from another renderer? For example, i am trying to implement custom render for some views in react-native and i want to provide context from main renderer (like graphql client). In short is there a way to implement something like portals but for custom renderer?

@gaearon
Copy link
Collaborator

gaearon commented Aug 21, 2018

I think your best option right now is to capture the context you need "above" the context boundary and then pass it down again with providers below the boundary.

@gaearon
Copy link
Collaborator

gaearon commented Nov 17, 2020

I added a non-exhaustive reference here.

https://github.com/facebook/react/tree/master/packages/react-reconciler#an-incomplete-reference

@iamandrewluca
Copy link

iamandrewluca commented Nov 5, 2021

@gaearon Is there any way to use a class that is not a Function component or a React.Component and reconciler to treat it as a host type?

I looked through reconciler API maybe there is some method that is called to check if a type is a host component type, but none found 👀


I started creating a custom renderer for AWS, Terraform, Kubernetes CDK constructs. This would allow to declare infrastructure using JSX syntax. see RFC

A Construct is a class that can extend another Construct and also can have inside created other Constructs. And there are already a lot of Constructs for different types of services
e.g. https://github.com/iamandrewluca/react-constructs/blob/3762ec26b1112fc835446b09db3ec9abdea68668/lib/hello-cdk-stack.tsx#L9-L21

a Construct constructor look like this

  constructor(scope: Construct, id: string, options: ConstructOptions = { }) {
  • scope is parent Construct
  • id unique id within current scope children
  • options the props for the Construct

So this basically maps very well usin JSX.

  • id can be used key prop
  • scope is parent JSX Element
  • options are the rest of the props

What I'm trying to achieve is this.
e.g. https://github.com/iamandrewluca/react-constructs/blob/3762ec26b1112fc835446b09db3ec9abdea68668/lib/hello-cdk-stack.tsx#L23-L42

The problem is that this Construct does not extend React.Component and react-reconciler detects it as a Function component and tries to call it like this

let children = Component(props, secondArg);

and it fails with

TypeError: Class constructor Stack cannot be invoked without 'new'

Stack is a construct

Is there any way to tell react-reconciler to treat this type as a host type, and call createInstance instead?

In the beginning I started to try to achieve this only using jsx runtime it works well, but we need the useLayoutEffect hook for imperative code, so a custom renderer is needed.

Thanks in advance!

ps: Didn't want to create another thread regarding react-reconciler questions

@gaearon
Copy link
Collaborator

gaearon commented Nov 6, 2021

No, only string types are treated as host components.

@iamandrewluca
Copy link

Thanks for your answer 🙂
Last night had an idea how to solve this. Somehow to duplicate the library that will export strings, that can be mapped back to original library. Will try it.

@iamandrewluca
Copy link

iamandrewluca commented Nov 6, 2021

I have one more question. So I moved to using strings:

export const Stack = "@aws-cdk/core/Stack";
export const Queue = "@aws-cdk/aws-sqs/Queue";

And I have this example:

export function HelloCdkStack(props) {
  return (
    <Stack {...props}>
      <Queue />
    </Stack>
  );
}

Because the creation of a Construct needs access to parent Construct. When createInstance is called for Queue, I need access to instance created for Stack, is this possible? Or the tree is created bottom-up? 🤔

When I do a type console.log in createInstance i get it bottom-up

{ type: '@aws-cdk/aws-sqs/Queue' }
{ type: '@aws-cdk/core/Stack' }

I tried playing with getChildHostContext but it seems I have access only to context (that is returned by getRootHostContext) and type that is a string, and not an instance

ps: I think I will just reimplement a simple jsx runtime, render function, useRef and useLayoutEffect, will be much easier 🤷🏼 https://github.com/iamandrewluca/constructs-jsx/blob/c8f3da4f86b75b5a2f168aa1052f055845fcb0a4/index.js#L15-L18

Thanks again for your answer!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants