react-navigation: connect with component/screen without props/state - reactjs

I'm looking for a way to connect the headerLeft and headerRight buttons of react-navigation with the screen/component it should be showed on, without using the props/state of that component.
We moved all our navigation transitions to one single service (at first because we needed it outside our react-components, but decided to replace it app-wide to keep one consistent flow).
Now, besides this service, we have few header-buttons, of which 2 require interaction with the screen: a cancel/finish and an edit button... Ideally, they would have a a simple callback which can be provided in the Component.
As suggested in the docs, this is the one class we use as a service:
import {StackActions, NavigationActions} from 'react-navigation';
import type {NavigationRoute, NavigationParams} from 'react-navigation';
let _navigator;
function setNavigator(navigatorRef) {
_navigator = navigatorRef;
}
function navigate(routeName: string, params?: NavigationParams) {
_navigator.dispatch(NavigationActions.navigate({routeName, params}));
}
function reset(routeName: string, params?: NavigationParams) {
//...
}
function getCurrentRoute(): NavigationRoute | undefined {
//...
}
function goBack() {
_navigator._navigation.goBack();
}
function getCurrentParams() {
return _navigator.state.params || {};
}
function setParams(params) {
_navigator._navigation.setParams(params);
}
export default {
//all functions
};
This is the button we have:
FinishCancelButton = () => (
<Button onPress={() => NavigationService.getCurrentParams().canFinish ? NavigationService.setParams({finish: true}) : NavigationService.setParams({cancel: true})}>
<Text>
{NavigationService.getCurrentParams().canFinish ? 'finish' : 'cancel'}
</Text>
</Button>
),
Before, the navigation object of the state of the component was passed, which would trigger state changes (canFinish from the component and pushback finish/cancel) and with componentWillReceiveProps (which is deprecated now) it updated the screen. But now this connection is gone, and we're looking for way to connect the two...
Ideally this would be a a simple callback:
FinishCancelButton = (onFinish) => (<Button onPress={onFinish} ... />)
But I don't see a way to do this. An other possible solution I've thought of is to attach a listener from the component to the service: which notifies when a certain property is changed.
Is there a way to create the button in the component itself, which can access the state?
Does anyone have any idea? Or if you have some more questions or other approaches, please share!

Related

Creating A React Component That Can Be Shown With A Function Call (like react-toastify

Typically, when creating a reusable React component that we want to conditionally render, we'll either give it a prop to tell it whether or not to render itself:
function TheComponent(props) {
return(
props.isVisible?<div>...</div>:null
);
}
Or just render the whole component conditionally, from the outside:
function App() {
//...
return (
isVisible ? <TheComponent /> : null
);
}
Alternatively, if we want to make a component that we can show/hide from anywhere in our application - like a toast notification - we could wrap in a provider & make a custom hook to access its context; this would let us show/hide it from anywhere inside the provider, just by calling a function:
const App = () => (
<ToastProvider>
<OtherStuff />
</ToastProvider>
);
const OtherStuff = () => {
const { showToast } = useToast();
showToast();
return ...;
};
However, there's a really cool package, react-toastify, that I can't seem to wrap my head around how it's implemented. All you have to do is drop a <ToastContainer /> somewhere in your app, then from anywhere else, you can:
import { toast } from "react-toastify";
toast.info("this will show the component with a message");
Since this function can be called outside of a provider, I don't really understand how it's controlling the state of the component elsewhere in the tree. I've tried looking into its code, but as a React beginner, it's a bit over my head. I love the idea of a totally self-contained component that you can just stick somewhere in your app, and invoke by calling a function from anywhere. No Provider/wrapper or anything: just a function call, and out it pops.
Can someone help shed some light on how, fundamentally, a component like this could work? How can a function outside of a provider be controlling state inside another component?
Glancing at the react-toastify code you can see it’s using an event emitter pattern. The <ToastContainer /> listens for events that get dispatched (or emitted) when you call toast.info. When it gets an event it updates its internal state (presumably) to show the message.
TLDR: They're communicating indirectly through the eventManager, which exposes methods for 1) dispatching events and 2) registering listeners for those events.
It's similar to the way an onclick handler works in the DOM.
Here's a very rudimentary implementation of the basic pattern: It just appends a div to the document each time the button is clicked. (This isn't React- or toastify-specific. But it demonstrates the core idea.)
Notice that the button's click handler doesn't know anything about what happens. It doesn't append the div. It just emits an event via the EventBus instance described below.
The EventBus class provides an on method to register a listener. These are often called addEventListener or addListener, or they have an event-specific name like onClick, onChange, etc., but they all do the same basic thing: register a function to be invoked in response to an event. (This class is essentially a dumber implementation of react-toastify's eventManager.)
The on method adds the provided handler to an internal array. Then, when an event is fired (via emit) it just iterates over the array invoking each one and passing in the event information.
const container = document.getElementById('demo');
const button = document.querySelector('button');
class EventBus {
handlers = [];
on (handler) {
this.handlers.push(handler);
}
emit (event) {
this.handlers.forEach(h => h(event));
}
}
const emitter = new EventBus();
emitter.on((event) => {
container.innerHTML += `<div>${event}</div>`;
})
button.addEventListener('click', () => emitter.emit('Button Clicked'));
<button>Emit</button>
<div id="demo"></div>
With this setup you can add additional listeners to do other things without having to know where the event originates (the button click). The demo below is the same as above except it adds an additional handler to toggle "dark" mode.
Again, notice that the button doesn't know about dark mode, and the dark mode handler doesn't know about the button, and neither of them know about the div being appended. They're completely decoupled.
This is basically how the ToastContainer works with toast.info.
const container = document.getElementById('demo');
const button = document.querySelector('button');
class EventBus {
handlers = [];
on (handler) {
this.handlers.push(handler);
}
emit (event) {
this.handlers.forEach(h => h(event));
}
}
const emitter = new EventBus();
emitter.on((event) => {
container.innerHTML += `<div>${event}</div>`;
})
button.addEventListener('click', () => emitter.emit('Button Clicked'));
// add an additional handler
emitter.on(event => demo.classList.toggle('dark'));
.dark {
background: black;
color: white;
}
<button>Emit</button>
<div id="demo"></div>

How do I call a function only when a React property changes?

I wish to show a modal dialog box (such as an alert()) every time a Meteor subscription, tracked in React with withTracker, changes.
I have tried using Tracker.autorun to track changes but cannot work out where in the code to place it. It does not seem to work in the Component constructor and runs every time if placed in render().
This is an outline of what my code looks like:
class Foo extends Component {
render() {
return (
<h1>Example Header</h1>
{ this.maybeShowAlert() }
);
}
maybeShowAlert() {
// ONLY if bar has been updated
alert('bar has changed');
}
}
export default withTracker(() => {
Meteor.subscribe('bar')
return {
bar: Bar.findOne({})
};
})(Foo);
Haven't used Meteor before, but if you want to do things in response to state/prop changes then componentDidUpdate() is the lifecycle method for it. E.g.
componentDidUpdate(prevProps) {
if (this.props.bar !== prevProps.bar {
// bar prop has changed
alert("bar changed);
}
}
If you're going to use Tracker.autorun, then the best place to call that is in componentDidMount, because it's called only once after the component has been mounted. You only need to call the tracker function once since the tracker function will rerun whenever the reactive data sources that it depends on ever changes. In the tracker function is where you will call maybeShowAlert depending on the value of bar like so,
componentDidMount() {
Tracker.autorun(() => {
let bar = this.props.bar;
if (bar) {
this.maybeShowAlert();
}
}
}

React Navigation - Get next screen name for another navigator

I have React Native app with React Navigation. I want to log screen name every time when it changes.
In my app I already have several stackNavigator's and one switchNavigator to combine them.
Now I've just added next onTransitionStart property to all stackNavigator's:
export default createStackNavigator(
{
...
},
{
...
onTransitionStart: transitionProps => {
console.log('move to: ', transitionProps.scene.route.routeName);
},
},
);
Mostly it helps and do it just I wanted. However, it doesn't work in cases when I move from one stackNavigator to another. Where it is a better way to leave the callback to know that we moved to another stackNavigator and get its first screen?
You can actually try the below method:
import { NavigationEvents } from 'react-navigation';
render() {
<NavigationEvents
onWillFocus={() => {
console.log('screenA')
}}
/>
}
Quoted from documentation
NavigationEvents is a React component providing a declarative API to subscribe to navigation events. It will subscribe to navigation events on mount, and unsubscribe on unmount.
For me helped next one: createAppContainer creates the container with onNavigationStateChange property. It takes a function with previous and next navigation state as arguments. Both of them have enough data to understand which screen was previous and which will be next one. Since I have this app container in my main App component, it looks next way:
<View>
/* ... */
<RootNavigator
/* ... */
onNavigationStateChange={(prevState, newState, action) => {
if (action.type === 'Navigation/NAVIGATE' || action.type === 'Navigation/BACK') {
const currentNavigator = newState.routes[newState.index];
const screenName = currentRoute.routes[currentNavigator.index].routeName;
console.log('Current screen is: ', screenName);
}
};}
/>
/* ... */
</View>
Both state objects store the index and routes properties. They represent chosen stackNavigator index and the list of stackNavigator's respectively. So we can get the number of chosen navigator and get the index of current route for it.

React "forgets" state when using debounce function

This is a question for someone who understands React/RN better than I. When I wrap touchable components (i.e. buttons) in a lightweight wrapper with a debounce function (to prevent onPress handler from being called too rapidly), it usually works as intended. HOWEVER, in a very specific set of circumstances, things go awry. If I pass a parent component's state down into the onPress prop of the button, if the state property I'm accessing has been destructured in the render method and then passed as this variable, rather than simply accessed as this.state.foo, the onPress handler reads it as its initial state value, even if it has been updated. This is probably confusing so allow me to show you a quick, incomplete example:
class DebounceButton extends Component {
handlePress = debounce(this.props.onPress, 500)
render() {
return (
<Button onPress={this.handlePress}
)
}
}
class Screen extends Component {
state = {
foo: 0
}
render() {
const { foo } = this.state
return (
<Button onPress={() => {this.setState({ foo: this.state.foo + 1 })}}/>
<DebounceButton onPress={() => {console.log(foo)}}/>
<DebounceButton onPress={() => {console.log(this.state.foo)}}/>
)
}
}
If I press Button, foo increments to 1. If I then press the first DebounceButton, the console will log 0. If I press the second, it will log 1 as it should. Note it only happens when state is passed through the debounce function AND assigned to a variable in the render method. It's as if React is "forgetting" what the current state is and default to its initial value. This is no longer a bug for me as I'm not using this debounce paradigm anymore, but I'm curious to understand it to better so I can have a better grasp on the way React works. Any insight is greatly appreciated.
The class property handlePress = debounce(this.props.onPress, 500) will only be evaluated when DebounceButton is first created, so changing the onPress after it has first been rendered does not work.
You could instead create a new function that invokes this.props.onPress inside of it. This way the current value of this.props.onPress will be used every time.
class DebounceButton extends Component {
handlePress = debounce(() => {
this.props.onPress();
}, 500);
render() {
return <Button onPress={this.handlePress} />;
}
}

How to change state of component from anywhere without Redux?

Is this bad practices or not ?
export state change function from component
import it from other file.
call the function to change state?
In this way we can change some component state from anywhere.
For example...
We want to change the Model.js state from anywhere.
Modal.js
import React from 'react';
export let toggleModal;
export default class Modal extends React.Component {
constructor(props) {
super(props);
this.state = {
open: false,
};
toggleModal = this.toggleModal;
}
toggleModal = () => {
this.setState({ open: !this.state.open });
};
render() {
const { open } = this.state;
return <div style={{ color: 'red' }}>{open && 'Hello Modal'}</div>;
}
}
App.js(Some Top Level component)
import React from 'react';
import Modal from './Modal';
export default () => (
<>
...
<Modal />
...
</>
);
Somewhere.js
import React from 'react';
import {toggleModal} from './Modal';
export default () => (
<>
<h1>Hello!</h1>
<button onClick={() => toggleModal()}>open Modal!</button>
</>
);
  
But there is no reference in React Official docs, so is this bad practices ?
What React Docs recommends...
Just passing function props to change parent state from parent to children
Use context
Redux or Mobx
But, these are too complex for me.
Example code here
https://next.plnkr.co/edit/37nutSDTWp8GGv2r?preview
Everything seems pretty much overwhelming and difficult at the beginning. But as we get out hands on them, it's give us more confidence to dig into.
I would recommend to use redux that's how we tackled props drilling problem. You can dispatch a action and connect reducer to corresponding component which upon updating state will re render. This is what I recommend to most of the people to learn the tale of redux with a real life example:
Understanding Redux: The World’s Easiest Guide to Beginning Redux
Apart from this you can take Dan Abramov, author of the library, free redux course on egghead.io:
Getting Started with Redux
The problem you run into, almost immediately like your code example does is this:
It will not work: your toggleModal() method expects a this to refer to an actual component instance. When your onClick() handler fires you invoke toggleModal() as a plain function. The this context will be wrong, and so at best (in your example) you will get an error because you try to invoke something undefined, at worst (in general) you end up invoking the wrong method.
When you think about it, for any non-trivial React component you will have a hard time obtaining a reference to the actual instance that is currently being used: you have to make sure that you are not forgetting to invoke the method on the right component instance and also you have to consider that instances may be created/destroyed 'at will' for whatever reason. For example: what if your component is rendered indirectly as part of some other component's render() method? Multiple layers of indirection like that make it even harder.
Now, you could fix all that by abusing ref with abandon but you will find that now you have to keep track of which ref refers to what particular instance, if you happen to have multiple of the components to consider in one render tree...
Whenever you think one component needs to handle the state of its siblings, the solution is usually to lift the state one level up.
export default class Modal extends React.Component {
render() {
const { isOpen } = this.props;
return <div style={{ color: 'red' }}>{isOpen && 'Hello Modal'}</div>;
}
}
export default class Home {
this.state = {
isOpen: false,
};
toggleModal = () => {
this.setState({ isOpen: !this.state.isOpen });
}
render() {
const { isOpen } = this.state;
return (
<>
<h1>Hello {name}!</h1>
<button onClick={() => this.toggleModal()}>open Modal!</button>
<Modal isOpen={isOpen}/>
<p>Start editing and see your changes reflected here immediately!</p>
</>
)
}
}
This way the Home handle the state and your problem is solved.
This can get annoying if the state needs to be "drilled down" to children, that's a problem than redux or react-context can solve.
Here <Modal /> is the child component. So to call a function in a child component you can simply use Ref.
You can refer this page to get more info about Ref.
You can assign a class variable as a ref to this child and use this class variable as an object to call its function.
I found if in special case, my way is okay.
Special case means something like customAlert component.
It is okay only one instance of customAlert component mounted at a time in App.
To achieve this...
1.Use ref to access and change DOM
2.attach state changing function or component to window and call window.function
3.my case: export state changing function and import it from other file.
And here is how to do with react Context
https://next.plnkr.co/edit/EpLm1Bq3ASiWECoE?preview
I think Redux is overkill if the main thing you are interested in is to make some states-like data available and updatable throughout your App without props drilling.
For that purpose, a much simpler approach (maybe not available at the time the question was posted?) is to use react context: https://frontend.turing.edu/lessons/module-3/advanced-react-hooks.html
"context - an API given to us by React, allowing for the passing of
information to child components without the use of props
[...]
useContext - a react hook, allowing functional components to take
advantage of the context API"

Resources