Open conrad-vanl opened 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.
@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
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;
},
}
@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).
@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
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.
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.
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;
},
}
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:
viewTag
(the native "viewTag" ID for the element you want to manage children for)moveFrom
- the index of the child element you want to movemoveTo
- the index you want to move the child element toaddChildTags
- an array of internal IDs of children you want to add to this elementaddAtIndices
- an array of indices you want to add the children to atremoveFrom
- an array of indices you want to remove children atSo 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:
manageChildren
(explained above)setChildren
(simplified version of manageChildren
)replaceExistingNonRootView
(replaces a view node....also just calls manageChildren
)removeSubviewsFromContainerWithID
deprecateddispatchViewManagerCommand
- allows you to call methods on other component classes. Such as calling a method on RCTView
directly, which is akin to a div
tag in HTML/Browser JS land. However, RCTView
(and similar) don't export any methods helpful to us (that I've found). We could write a custom RCTReparentable
class though that did...updateView
- allows you to update a view's props on the native sidefindSubviewIn
or viewIsDescendantOf
measure
which gets width/height, etcbut 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.
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?
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
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?