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

Pass function callback as props to scenes #322

Closed
emerson233 opened this issue Nov 7, 2019 · 21 comments
Closed

Pass function callback as props to scenes #322

emerson233 opened this issue Nov 7, 2019 · 21 comments

Comments

@emerson233
Copy link
Contributor

emerson233 commented Nov 7, 2019

Hello, it's me again.
One question, how do we pass function type props to scenes?
I am doing something like this:

const stateNavigator = new StateNavigator([
  {key: 'sceneA', trackCrumbTrail: true},
  {key: 'sceneB', trackCrumbTrail: true},
]);
const {sceneA, sceneB} = stateNavigator.states;
sceneA.renderScene = () => <Foo />;
sceneB.renderScene = ({callback}) => <Bar />;

stateNavigator.navigate("sceneB", {
  callback: () => {
    // change something
  }
})

However, the value for callback in sceneB.renderScene is undefined.
It seems function type variable is erased from navigationData when encoding data to routeInfo and decoding it back from routeInfo?

@grahammendick
Copy link
Owner

Hey, great to hear from you again. You can't pass functions in navigation data. If you let me know your use-case then I'll try and help out

@emerson233
Copy link
Contributor Author

The navigation library(react-native-navigation) we used before can pass function call to its "screens". I am planning to build a feature similar to this one for backward compatibility.
Please let me know If you have a better idea.

@grahammendick
Copy link
Owner

Let me see if I understand what you're trying to do. If you navigate from A --> B --> C then you want to update B's navigation data when you're on C. Is that right?

@emerson233
Copy link
Contributor Author

Yes, do you have any advice for this scenario?

@grahammendick
Copy link
Owner

grahammendick commented Nov 8, 2019

Yes, you can do that using fluent navigation. Let's say that when you first navigated from A to B you passed 'x' with a value of 1 in navigation data

// On Scene A
stateNavigator.navigate('B', {x: 1});

Now, you're on C and you want to change the 'x' navigation data to 3 on scene B.

// On Scene C
var link = stateNavigator.fluent(true)
  .navigateBack(2)
  .navigate('B', {x: 3})
  .navigate('C').url;
stateNavigator.navigateLink(url|);

You see, fluent navigation lets you perform multiple navigations at once. Here we're navigating back 2 scenes to A, then navigating forward to B and passing the new value for x, then navigating forward to C again. The Navigation router only applies these navigations when you call navigateLink. It applies these changes in the most efficient way possible. So the user won't see any changes until they tap the back button.

@grahammendick
Copy link
Owner

Sorry, I made a mistake in the fluent navigation code sample. I've corrected it, so please give it a reread

@emerson233
Copy link
Contributor Author

Seems promising, but still not flexible enough compared to passing callback.

Consider this scenario: In scene B, user click a button navigating to scene C. If scene C is displaying a dialog which contains two button for user to click(cancel/confirm). After user clicks one of them, we do as following:

// On Scene C
var link = stateNavigator.fluent(true)
  .navigateBack(2)
  .navigate('B', {clickedButtonIndex: 0});
stateNavigator.navigateLink(url|);

And user navigateBack to scene B now. Then user click another button navigating to scene D, which is the same component as scene C. If user clicks cancel/confirm button in scene D, then we have to figure out where is this click event from (try not to mess up with scene C).

@grahammendick
Copy link
Owner

I'm confused. Can you explain what you can do with the callback that you don't think you can do with fluent navigation, please?

@emerson233
Copy link
Contributor Author

Sorry, since I am not a native english speaker, it's hard to make myself clear.
Fluent navigation is capable of doing everything as you said, but it's making codebase complicated and hard to understand.

@grahammendick
Copy link
Owner

I wouldn't use fluent navigation to tell screen B that you clicked a button on screen C. You should only use fluent navigation to change navigation data.

If you want to tell screen B that you clicked a button then you should use React context. React context is the way to communicate and pass data between scenes.

@emerson233
Copy link
Contributor Author

Understood. Thanks for your explaining.

@grahammendick
Copy link
Owner

No problem. Thanks for the question.

You couldn't use React Context with react-native-navigation because each scene is mounted under a different React root element.

You can use React Context with the Navigation router because all scenes share a common React root element.

@studyroz
Copy link
Contributor

IMO, being able to pass function as data helps to increase flexibility.

Consider a selectable user list scene, which may be used in many places in an app.

stateNavigator.navigate('users-selector', {
  title: "Contacts",
  fetchData: () => {
    // Fetch from contacts
  },
  renderListItem: (user) => {
    // Render a contact list item
  },
  onSelected: (users) => {
  }
})

// Another place:
stateNavigator.navigate('users-selector', {
  title: "Team Members",
  fetchData: () => {
    // Fetch from team members
  },
  renderListItem: (user) => {
    // Render a member list item
  },
  onSelected: (users) => {
  }
})

The 'users-selector' scene handles selection state management, template header & bottom components rendering and so on. By passing custom action as function through data, we can make users-selector scene very flexible.

@grahammendick
Copy link
Owner

Hey @studyroz It's better to do things like that with React than with the Navigation router. For example, you can wrap the UserSelector component in two different Higher Order Components like Contacts and TeamMembers components. Then you can pass a renderListItem prop to he UserSelector component

@mpiannucci
Copy link
Contributor

mpiannucci commented Nov 12, 2019

@grahammendick I disagree. Its a really simple pattern that is way clearer than having to wrap everything in a context/ HOC. I'm not talking about the callbacks for fetching data but moreso the ones @studyroz defined for onSelected. That's a pattern I would use a lot for a settings screen for instance.

I would argue that this hurts reusability too because as I understand it you cant have props that are callbacks when using a screen? So when designing you have to change the way you would normally write something to conform to the limitations of the Navigation functionality

@grahammendick
Copy link
Owner

@mpiannucci Sounds like you’re looking for a convenient way to pass data back from Scene B to Scene A, right? I’ll try and explain why I don’t think that’s a good idea. I’ll use Hooks because it makes the problem more obvious. Here’s Scene A. It displays a count and has a button that navigates to Scene B. When it navigates to Scene B it passes a callback that adds 1 to the count.

const SceneA = () => {
  const {count, setCount} = useState(0)
  const {stateNavigator} = useContext(NavigationContext);
  const add = () => setCount(count + 1)
  return (
    <>
      <Text>{count}</Text>
      <Button onPress={() => stateNavigator.navigate('sceneB', { add }) />
    </>	
  )
}

Let’s say that Scene B calls the add callback twice in a row. The count should now be 2, but when you navigate back to Scene A the count is actually only 1. That’s because the add callback on Scene B closed over the count when it was 0. When Scene B calls add the first time, the add callback is recreated in Scene A and closes over the new count of 1. But Scene B still has the old value for the add callback which closed over the count when it was 0. So when Scene B calls add a second time it sets a value of 1.

@mpiannucci
Copy link
Contributor

Aha good call. Thanks for the explanation. Some of this is new to me coming from c++ and swift where the flow is a little different

@mpiannucci
Copy link
Contributor

mpiannucci commented Apr 28, 2020

So I am hitting this again. I am trying to display a modal screen where a user can select an item, the modal is dismissed, and then that item is presented in the main view. I forgot this wasn't possible and was going crazy trying to figure out why the callback wasn't working.

Is there any technical limitation to why passing callbacks isn't supported? I know the case with setting the count and such but my flow deals with strings so i am strictly trying to present a one off select and then closing the dialog.

I want the native search bar functionality, otherwise i wouldn't give the modal its own navigation context. I am aware I can use context to manage this state but it would be a lot cleaner not to.

@grahammendick
Copy link
Owner

You don’t have to use context. You can pass the function as a prop.

const [val, setVal] = useState(null);
const modalNavigator = useMemo(() => {
  const navigator = new StateNavigator([
    {key: ‘search’}
  ]);
  const {search} = navigator.states;
  search.renderScene = () => <Search setVal={setVal} />
  navigator.navigate('search');
  return navigator;
}, [setVal]);
return (
  <Modal>
    <NavigationHandler stateNavigator={modalNavigator}>
      <NavigationStack />
    </NavigationHandler>
  </Modal>
);

@mpiannucci
Copy link
Contributor

So you can pass a function as long as you pass it to render scene. Great thanks for the help!!!!!

@grahammendick
Copy link
Owner

grahammendick commented Jan 6, 2022

Here’s a more complete example of how you can use props, instead of context, to pass data from a parent scene to a modal scene (make sure you’re using the just published navigation-react-native v8.7.1)

const Wrapper = ({state, ...props}) => state.renderScene(props);

const ParentScene = () => {
  const [val, setVal] = useState();
  const modalNavigator = useMemo(() => {
    const navigator = new StateNavigator([{key: 'scene'}])
    const {scene} = stateNavigator.states;
    scene.renderScene = (props) => <ModalScene {...props} />;
    navigator.navigate('scene');
    return navigator;
  }, []);

  return (
    <NavigationHandler stateNavigator={modalNavigator}>
      <NavigationStack renderScene={(state) => (
        <Wrapper state={state} val={val} setVal={setVal} />
      )} />
    </NavigationHandler>
  );
}

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

4 participants