React Native HOC with nested react-navigation - reactjs

I have a HOC used to pass react's Context as props to any component (a similar functionality and structure to redux):
import React from 'react';
import { GlobalContext } from './context';
export const globalContext = (mapStateToProps = state => ({ ...state })) => Children => props => (
<SignUpContext.Consumer>
{state => (<Children {...mapStateToProps(state)} {...props} />)}
</SignUpContext.Consumer>
);
This works as expected in normal components but when I try to use with a nested react navigator an error is thrown because cant access to the static router.
If I use either navigation or HOC It works but I can't manage to make both of them work
This would be a theoretical example of HOC and navigation:
import globalContext from './context'
import Screen from './screen';
import Screen2 from './screen2';
const MainStack = createBottomTabNavigator({
Screen: {screen: globalContext()(Screen)},
Screen2,
});
export default class Main extends Component {
static router = MainStack.router;
render() {
console.log(this.props.navigator); //does exist
return (
<View >
{ /*some component over all navigator*/}
<MainStack navigator={this.props.navigator} />
</View>
)
}
}
ok, that failed but while I was editing I realized that It was stupid to use the class so it worked just exporting the createBottomTabNavigator

Wrap each screen by HOC then pass it to navigation may be helped.
import globalContext from './context'
import Screen from './screen';
import Screen2 from './screen2';
const MainStack = createBottomTabNavigator({
globalContext()(Screen),
globalContext()(Screen2),
});
export default MainStack;

Related

How to use refs in react through react-redux , withRouter?

I'm trying to use ref in a component connected to react-redux
I've tried this solution.
connect(null, null, null, {forwardRef: true})(myCom)
<myCom ref={ref => this.myCom = ref} />
this works just fine according to react-redux docs, but now when i try using
withRouter at the same time i get an error:
Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
so the final export code i have tried which lead to the above error
export default connect(null, null, null, { forwardRef: true })(withRouter(withStyles(styles)(myCom)));
Note: withStyles doesn't cause any issues as i've tried removing only withRouter, the issue was resolved.
is there any solution to solve this issue ?
In order to pass a ref to a component wrapped by withRouter you need to call it wrappedComponentRef. I recommend having withRouter as the outermost wrapper, so your example would look like the following:
withRouter(connect(null, null, null, {forwardRef: true})(myCom));
<myCom wrappedComponentRef={ref => this.myCom = ref} />
The following example and description are adapted from a related answer of mine: Get ref from connected redux component withStyles
Below is code from a modified version of the react-redux todo list tutorial that shows the correct syntax. I've included here the two files that I changed (TodoList.js and TodoApp.js), but the sandbox is a fully working example.
In TodoApp, I use the ref (via the wrappedComponentRef property) on TodoList to get and display its height. The displayed height will only get updated if TodoApp re-renders, so I've included a button to trigger a re-render. If you add a couple todos to the todo list, and then click the re-render button, you will see that the new height of the list is displayed (showing that the ref is fully working).
In TodoList, I'm using withStyles to add a blue border around the todo list to show that withStyles is working, and I'm displaying the primary color from the theme to show that withTheme is working. I am also displaying the location object from withRouter to demonstrate that withRouter is working.
TodoList.js
import React from "react";
import { connect } from "react-redux";
import Todo from "./Todo";
import { getTodosByVisibilityFilter } from "../redux/selectors";
import { withStyles, withTheme } from "#material-ui/core/styles";
import clsx from "clsx";
import { withRouter } from "react-router-dom";
const styles = {
list: {
border: "1px solid blue"
}
};
const TodoList = React.forwardRef(
({ todos, theme, classes, location }, ref) => (
<>
<div>Location (from withRouter): {JSON.stringify(location)}</div>
<div>theme.palette.primary.main: {theme.palette.primary.main}</div>
<ul ref={ref} className={clsx("todo-list", classes.list)}>
{todos && todos.length
? todos.map((todo, index) => {
return <Todo key={`todo-${todo.id}`} todo={todo} />;
})
: "No todos, yay!"}
</ul>
</>
)
);
const mapStateToProps = state => {
const { visibilityFilter } = state;
const todos = getTodosByVisibilityFilter(state, visibilityFilter);
return { todos };
};
export default withRouter(
connect(
mapStateToProps,
null,
null,
{ forwardRef: true }
)(withTheme(withStyles(styles)(TodoList)))
);
TodoApp.js
import React from "react";
import AddTodo from "./components/AddTodo";
import TodoList from "./components/TodoList";
import VisibilityFilters from "./components/VisibilityFilters";
import "./styles.css";
export default function TodoApp() {
const [renderIndex, incrementRenderIndex] = React.useReducer(
prevRenderIndex => prevRenderIndex + 1,
0
);
const todoListRef = React.useRef();
const heightDisplayRef = React.useRef();
React.useEffect(() => {
if (todoListRef.current && heightDisplayRef.current) {
heightDisplayRef.current.innerHTML = ` (height: ${
todoListRef.current.offsetHeight
})`;
}
});
return (
<div className="todo-app">
<h1>
Todo List
<span ref={heightDisplayRef} />
</h1>
<AddTodo />
<TodoList wrappedComponentRef={todoListRef} />
<VisibilityFilters />
<button onClick={incrementRenderIndex}>
Trigger re-render of TodoApp
</button>
<div>Render Index: {renderIndex}</div>
</div>
);
}
Use compose method and try something like this
const enhance = compose(
withStyles(styles),
withRouter,
connect(mapStateToProps, null, null, { forwardRef: true })
)
and use it before exporting component
export default enhance(MyComponent)
You can do it this way! This will work for sure👍
import { withRouter } from 'react-router';
//Just copy and add this withRouterAndRef HOC
const withRouterAndRef = (WrappedComponent) => {
class InnerComponentWithRef extends React.Component {
render() {
const { forwardRef, ...rest } = this.props;
return <WrappedComponent {...rest} ref={forwardRef} />;
}
}
const ComponentWithRouter = withRouter(InnerComponentWithRef, { withRef: true });
return React.forwardRef((props, ref) => {
return <ComponentWithRouter {...props} forwardRef={ref} />;
});
}
class MyComponent extends Component {
constructor(props) {
super(props);
}
}
//export using withRouterAndRef
export default withRouterAndRef (MyComponent)

Simple passing of Value from one Component to another through context

I'm new to react native and would like to use Context to keep a socket connection alive between screens in the future. For now, I tried to learn the concept of context just to pass simple values around but the value doesn't get sent.
Tried to follow the tutorial here, but by sending simple values instead.
I create my ValueContext in ValueContext.js here.
import React from 'react';
const ValueContext = React.createContext();
export default ValueContext;
Here's my LoginScreen.js where I set context provider.
import React, { Component } from 'react';
import ConnectionScreen from './ConnectionScreen';
import ValueContext from './ValueContext';
const testValue = 5;
export const sendValue = props => (
<ValueContext.Provider value={testValue}>
<ConnectionScreen />
</ValueContext.Provider>
)
class LoginScreen extends Component {
render() {
return()
}
}
Then in my ConnectionScreen.js
import React, { Component } from 'react';
import { View, Alert } from 'react-native';
import LoginScreen from './LoginScreen';
import ValueContext from './ValueContext';
export const receiveValue = props => (
<ValueContext.Consumer>
{testValue => <ConnectionScreen {...props} testValue={testValue} />}
</ValueContext.Consumer>
)
class ConnectionScreen extends Component {
showAlertValue = () => {
Alert.alert(this.props.testValue);
}
render() {
return(
<View>
{this.showAlertValue()}
</View>
)
}
}
So after setting the value in LoginScreen, I would like to access it in ConnectionScreen. All I get in my alert box is an empty box with no values. Am I doing something wrong here?

How should the new context api work with React Native navigator?

I created a multiscreen app using React Navigator following this example:
import {
createStackNavigator,
} from 'react-navigation';
const App = createStackNavigator({
Home: { screen: HomeScreen },
Profile: { screen: ProfileScreen },
});
export default App;
Now I'd like to add a global configuration state using the new builtin context api, so I can have some common data which can be manipulated and displayed from multiple screens.
The problem is context apparently requires components having a common parent component, so that context can be passed down to child components.
How can I implement this using screens which do not share a common parent as far as I know, because they are managed by react navigator?
You can make it like this.
Create new file: GlobalContext.js
import React from 'react';
const GlobalContext = React.createContext({});
export class GlobalContextProvider extends React.Component {
state = {
isOnline: true
}
switchToOnline = () => {
this.setState({ isOnline: true });
}
switchToOffline = () => {
this.setState({ isOnline: false });
}
render () {
return (
<GlobalContext.Provider
value={{
...this.state,
switchToOnline: this.switchToOnline,
switchToOffline: this.switchToOffline
}}
>
{this.props.children}
</GlobalContext.Provider>
)
}
}
// create the consumer as higher order component
export const withGlobalContext = ChildComponent => props => (
<GlobalContext.Consumer>
{
context => <ChildComponent {...props} global={context} />
}
</GlobalContext.Consumer>
);
On index.js wrap your root component with context provider component.
<GlobalContextProvider>
<App />
</GlobalContextProvider>
Then on your screen HomeScreen.js use the consumer component like this.
import React from 'react';
import { View, Text } from 'react-native';
import { withGlobalContext } from './GlobalContext';
class HomeScreen extends React.Component {
render () {
return (
<View>
<Text>Is online: {this.props.global.isOnline}</Text>
</View>
)
}
}
export default withGlobalContext(HomeScreen);
You can also create multiple context provider to separate your concerns, and use the HOC consumer on the screen you want.
This answer takes in consideration react-navigation package.
You have to wrap your App component with the ContextProvider in order to have access to your context on both screens.
import { createAppContainer } from 'react-navigation'
import { createStackNavigator } from 'react-navigation-stack'
import ProfileContextProvider from '../some/path/ProfileContextProvider'
const RootStack = createStackNavigator({
Home: { screen: HomeScreen },
Profile: { screen: ProfileScreen },
});
const AppContainer = createAppContainer(RootStack)
const App = () => {
return (
<ProfileContextProvider>
<AppContainer />
</ProfileContextProvider>);
}
https://wix.github.io/react-native-navigation/docs/third-party-react-context/
As RNN screens are not part of the same component tree, updating the values in the shared context does not trigger a re-render across all screens. However you can still use the React.Context per RNN screen component tree.
If you need to trigger a re-render across all screens, there are many popular third party libraries such as MobX or Redux.

How to pass props to 'screens'/components in react-navigation

I'm fairly new to programming in general and even newer to JS and React(Native) but I have worked on this for an entire day now and I still haven't figured it out so I have resorted to Stack Overflow in hopes that someone can help me.
Basically what I want to accomplish is to set other Components as children of the App component because I want them to be able to access information that I will set in the state of App. However, at the same time, I am also using react-navigation to create bottom navigation bars and thus I have no idea on how I can pass props of App to these other Components such as the ExplorePage component which is representative of the other children components.
App
import React from 'react';
import ExplorePage from './app/tabs/ExplorePage';
import {createBottomTabNavigator} from 'react-navigation';
...other imports
class App extends React.Component {
state = {
parentState: 'testing testing',
}
}
const MainScreenNavigator = createBottomTabNavigator(
{
Home: {screen: ExplorePage},
Search: {screen: SearchPage},
Favorites: {screen: FavoritesPage},
}
);
export default MainScreenNavigator;
ExplorePage, which is just like SearchPage and FavoritesPage
...imports
export default class ExplorePage extends React.Component {
constructor(props) {
super(props);
this.state = {
}
}
componentDidMount() {
console.log(this.props.parentState ? this.props.parentState : "Parent state does not exist what do :(");
}
render(){
return(
<Text>Testing</Text>
)
}
And obviously every time the console prints that parentState does not exist. I thought that being in the same place would give the other components like ExplorePage props of App. Thanks for helping me!
for those who are looking for a React Navigation 5 solution, you can use initialParams like this:
<Stack.Navigator>
<Stack.Screen
name="screenName"
component={screenComponent}
initialParams={{key: value}}
/>
</Stack.Navigator>
You could pass a props using function. Try this
import React from 'react';
import ExplorePage from './app/tabs/ExplorePage';
import {createBottomTabNavigator} from 'react-navigation';
...other imports
class App extends React.Component {
state = {
parentState: 'testing testing',
}
render() {
// old
// const MainScreenNavigator = mainScreenNavigator(this.state.parentState);
const MainScreenNavigator = mainScreenNavigator(this.state);
return (
<MainScreenNavigator />
)
}
}
const mainScreenNavigator = value => createBottomTabNavigator(
{
// Home: { screen : props => <ExplorePage {...props} parentState={value} /> },
Home: { screen : props => <ExplorePage {...props} {...value} /> },
Search: {screen: SearchPage},
Favorites: {screen: FavoritesPage},
}
);
export default App;
Edit
First thing, I changed your MainScreenNavigator to be a function, as it is accepting state values dynamically.
Second thing, Instead of directly assigning { screen : Component }, I used function. This is the feature provided by reactnavigation. You can find about this in the documentation. ReactNavigation
If you want to pass multiple attributes then you can use es6 spread operator, as shown in the edit. {...value}, this will pass all the property of value to that component.
You should use Navigator Props "screenProps" as mentionned in API:
screenProps - Pass down extra options to child screens
On child screen, just take props via this.props.screenProps

Changing material-ui theme on the fly --> Doesn't affect children

I'm working on a react/redux-application where I'm using material-ui.
I am setting the theme in my CoreLayout-component (my top layer component) using context (in accordance to the documentation). This works as expected on initial load.
I want to be able to switch themes during runtime. When I select a new theme, my redux store gets updated and therefore triggers my components to update. The problem is that the children of my CoreLayout-component doesn't get affected - the first time! If I repeatedly change my theme (using a select-list that sends out a redux-action onChange) the children are updated. If a child component is located 2 layers down in my project hierarchy, it is updated after 2 action calls - so there is some issue with how the context is passed down.
My CoreLayout.js component
import React, { PropTypes } from 'react';
import { connect } from 'react-redux';
import ThemeManager from 'material-ui/lib/styles/theme-manager';
const mapStateToProps = (state) => ({
uiStatus: state.uiStatus
});
export class CoreLayout extends React.Component {
getChildContext() {
return {
muiTheme: ThemeManager.getMuiTheme(this.props.uiStatus.get("applicationTheme").toJS())
};
}
render () {
return (
<div className='page-container'>
{ this.props.children }
</div>
);
}
}
CoreLayout.propTypes = {
children: PropTypes.element
};
CoreLayout.childContextTypes = {
muiTheme: PropTypes.object
};
export default connect(mapStateToProps)(CoreLayout);
One of my child components (LeftNavigation.js)
import React from "react";
import { connect } from 'react-redux';
import { List, ListItem } from 'material-ui';
const mapStateToProps = (state) => ({
uiStatus: state.uiStatus
});
export class LeftNavigation extends React.Component {
render () {
return (
<div className="left-pane-navigation">
<List subheader="My Subheader" >
<ListItem primaryText="Search" />
<ListItem primaryText="Performance Load" />
</List>
</div>
);
}
}
LeftNavigation.contextTypes = {
muiTheme: React.PropTypes.object
};
export default connect(mapStateToProps)(LeftNavigation);
I can access the theme located in context by this.context.muiTheme.
I can get the component to update the theme by using another instance of getChildContext() inside each child component, but I will have such a large number of components that I would very much like to avoid having to do that.
My CoreLayout component's getChildContext-method is called when I change theme and all my child components gets re-rendered as expected.
Any ideas?
Update: It works as expected on mobile devices (at least iOS)
You can use muiThemeProvider to avoid having to add getChildContext to any child component, the provider does this for you.
...
import getMuiTheme from 'material-ui/styles/getMuiTheme';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
import MyAwesomeReactComponent from './MyAwesomeReactComponent';
const App = () => (
<MuiThemeProvider muiTheme={getMuiTheme()}>
<MyAwesomeReactComponent />
</MuiThemeProvider>
)
...
More info in documentation.

Resources