paol-imi / react-reparenting

The reparenting tools for React
https://paol-imi.github.io/react-reparenting
MIT License
481 stars 8 forks source link

Any ideas on where to start for react-native support #7

Open conrad-vanl opened 4 years ago

conrad-vanl commented 4 years ago

Great work on this! Would be interested in helping bring support to react-native, if you think it could be possible. Any ideas on where to start?

paol-imi commented 4 years ago

Thank you very much for your appreciation and support.

I'm almost sure it's possible to implement it with React-Native, but unfortunately I've never used it. I have tried to leave the APIs as open as possible and it should be easy to find an implementation.


This is the default configuration that works with ReactDOM, it can be easily [**changed**](https://paol-imi.github.io/react-reparenting/docs/renderer) and theoretically, it would only be necessary to find the native equivalent of DOM elements and methods such as appendChild. Once configured, the package should work even now without any other changes. I guess what we are looking for is not included in the official documentation, the approach I would use would be to try the package with React-Native and use debug to study the instances that are passed to the **isElement** function.
conrad-vanl commented 4 years ago

@Paol-imi thanks. I've started doing this. react-native's renderer is quite a bit different. However, I did find the internal renderer methods on react-native's end that could be helpful:

So far, I've been able to get remove to work, but so far append and insert don't appear to do anything:

configure({
  appendChildToContainer(container, child) {
    const children = container._children;
    const index = children.indexOf(child);

    if (index >= 0) {
      children.splice(index, 1);
      children.push(child);
      UIManager.manageChildren(
        container._nativeTag,
        [index],
        [children.length - 1],
        [],
        [],
        []
      );
    } else {
      children.push(child);
      UIManager.manageChildren(
        container._nativeTag,
        [],
        [],
        [child._nativeTag],
        [children.length - 1],
        []
      );
    }
  },
  removeChildFromContainer(container, child) {
    const children = container._children;
    const index = children.indexOf(child);
    children.splice(index, 1);
    UIManager.manageChildren(container._nativeTag, [], [], [], [], [index]);
  },
  isElement(elementType, stateNode) {
    return stateNode && stateNode._nativeTag;
  },

This is of course using internal properties, which probably goes against your rules for the package. I'll keep digging. It looks like others that have attempted similar on react-native have been unsuccessful.

I have a feeling this is failing because the tag that's being removed no longer exists on the native side, and so the append call makes no sense to the native renderer since that tag no longer exists.

Might have to try and figure out how to call both remove and append in the same message that's sent over the bridge

paol-imi commented 4 years ago

This is of course using internal properties, which probably goes against your rules for the package. I'll keep digging.

In the article I wanted to specify that I would not use APIs that I have not designed and of which I cannot be sure of the correct functioning (like the one named with the unstable_ tag). In this way, If there are problems or bugs, the resolution process will not be constrained by any external method that I cannot modify. Using these Internals does not go against this concept, since if they were modified (which I believe is very rare in the same major version) the logic of the package will not have to be rewritten, but only the configuration changed.

Therefore, if I understand correctly, you believe that the problem lies during the call

UIManager.manageChildren(container._nativeTag, [], [], [], [], [index]);

inside removeChildFromContainer which destroys/invalidates the native tag. Personally I would try any type of hacky way to test it, without worrying about how elegant it is. Once a solution is found, if it requires a redesign of the package, large or small, it can be done without problems.

For example if you want to try to remove the tag only after inserting it in the new container.

let oldContainer;

configure({
  appendChildToContainer(container, child) {
    // Append 
    // and then Remove from 'oldContainer'
  },
  removeChildFromContainer(container, child) {
    oldContainer = container;
  },
}
conrad-vanl commented 4 years ago

@Paol-imi yep, that sounds in line with the things I was going to try. I can confirm that appending before removing causes a crash, but I’ll keep trying some things and exploring.

This package would be huge for the react-native community (well, huge for the few of us that would greatly benefit from this).

conrad-vanl commented 4 years ago

@Paol-imi i have a feeling there's a higher level conceptual outage on my end.

If I recreate the demo you specify here - https://paol-imi.github.io/react-reparenting/docs/reparentable - and add console.log to each of the methods inside of configure, those logs only get called when I use sendReparentableChild, but they don't when I try to use the state hook updater setParents

paol-imi commented 4 years ago

sendReparentableChild allows you to change the current state of the App. If you want a Child component somewhere else, calling the method allows you to synchronize your App with that modification, consequently the DOM is updated.

In the subsequent re-rendering React will behave as if the transferred Child had always been there.

conrad-vanl commented 4 years ago

Just as an update, I wasn't able to get anywhere on this without having to resort to writing native code, which might be the only way to get this to truly work on react-native at this time.

At least on iOS, I've traced down to this line of code as being the main issue.

Essentially, if RCTUIManager detects that a node is being removed (instead of moved) on a given function call, it purges the node instead of simply un-rendering it. If you try to append first then remove, the app crashes (I assume since it's trying to render the same node twice).

As far as I can tell, there's nothing within this module that would allow for reparenting to work. Android might be different, but I doubt it.

Furthermore, I'm not sure if your other fabric based functions are working entirely correctly on react-native. Like I said above, the only way I could get your library code to trigger at all is by using sendReparentableChild command. Updating state or moving children around outside of that function, like in your demos, causes those children to remount and none of your library code ever gets called.

paol-imi commented 4 years ago

Updating state or moving children around outside of that function, like in your demos, causes those children to remount and none of your library code ever gets called.

Let me be clearer: The Reparentable component has the only purpose of allowing/managing the operation of the sendReparentableChild function. If you move some Child without using the method, React will behave normally by unmounting the Child.

sendReparentableChild allows you to transfer the Child at the React level, so that in the next re-rendering React will behave as if the child had always been there. Obviously, in this process, React expects the DOM element or native tag to be in the correct location, so they must be transferred.

You can see Reparenting as sendReparentableChild + re-rendering. you should see this as a single atomic operation, the fact that there is no single method is only a limitation of the current implementation, maybe in the future I will be able to implement something more automated.

Indeed, the configuration methods allow you to easily interact with the native tags only following the use of the sendReparentableChild method.

Essentially, if RCTUIManager detects that a node is being removed (instead of moved) on a given function call, it purges the node instead of simply un-rendering it.

When you say "instead of moved" do you mean that you have found a piece of code that distinguishes the moved tags from the removed ones and treats them differently? This makes me think that there is a possible solution using only javascript.

I don't know if writing native code can help you in your intent, but I'm pretty sure that the solution also requires some interactions with RCTUIManager


When you wrote the first part of the code (now it's different) it seemed to me that there were some inconsistencies, I saw the UIManager.manageChildren method in both append and remove and then UIManager.setChildren only in append and no similar alternative in remove.

Out of curiosity, have you ever tried to do something like this?

let oldContainer;

configure({
  appendChildToContainer(container, child) {
    UIManager.setChildren( container._nativeTag, /***/ )
    UIManager.manageChildren( oldContainer._nativetag, /***/  ) // Remove
    UIManager.manageChildren( container._nativeTag, /***/  ) // Append
  },
  removeChildFromContainer(container, child) {
    oldContainer = container;
  },
}
conrad-vanl commented 4 years ago

I understand now. Some of your explanation in the first part of your comment sounds different then what I remember reading in the docs, but I probably didn't understand.

In another comment, you mentioned:

Once you've set it up, you can just re-render the components.

This made it sound like you didn't have to use sendReparentableChild. From your response above, I understand that you have to call sendReparentableChild first, then you can update state?

When you say "instead of moved" do you mean that you have found a piece of code that distinguishes the moved tags from the removed ones and treats them differently?

Essentially. I know the objective-C can be hard to follow, especially if you've never written objC before, but essentially there is really only 1 function that gets exported that's helpful for us (that I've found): UIManager.manageChildren. The Java Version on Android might be easier to read.

The function has 6 arguments you can pass to it:

So when I say "instead of moved" - if you are removing a child - it will destroy that child immediately, such as using the removeFrom argument. If you are moving it - say using the moveFrom and moveTo arguments - it will un-render it but not destroy it, then render it at the new position in the children array.

If you try to add the child to a new container before removing it from the old container, the app will crash. If you remove it before adding it the element never shows back up. Forcing a re-render at this point causes the element to re-appear at the old position in the tree.

Typing the above out... perhaps there might be possibility in removing from old container then appending it to the new container on the native side, then getting the javascript side to re-render with the child in the new container...but I'd still have to do it in a way that react doesn't try to remount the child...

When you wrote the first part of the code (now it's different) it seemed to me that there were some inconsistencies

Correct. I had a mistake in that code - I was experimenting with using setChildren as well as manageChildren. The version I copied into github used both. It was a mistake. UIManager.setChildren internally that just calls manageChildren anyways, so there's no functional difference.

The methods available for us by RCTUIManager are:

paol-imi commented 4 years ago

but I'd still have to do it in a way that react doesn't try to remount the child

SendReparentableChild does exactly that. The only problem to solve is to transfer the native tags, through js or native code it doesn't matter. All the work concerning React is done by the package.

At this point, however, I doubt that through the RCTUIManager it is not possible without invalidating the tag.

I think the choice I would make would be to try to reuse the Native code of the RCTUIManager for the append, remove and insertBefore methods. If in the remove function you could find and delete the part of code that invalidates the native tag it could work.

conrad-vanl commented 4 years ago

I think the choice I would make would be to try to reuse the Native code of the RCTUIManager for the append, remove and insertBefore methods. If in the remove function you could find and delete the part of code that invalidates the native tag it could work.

Yep, this is exactly where my thought process ended up too. Will require writing a small amount of native code though.

Would you still want to consider a PR against your package if it included some native code to work?

paol-imi commented 4 years ago

Of course, I'd love to.

I think the best approach would be to have an optional configuration to import, which is not directly imported into the package.

import 'react-reparenting/react-native-configuration';

In case, if you want, I will add a CONTRIBUTORS.md file