Call lifecycle methods on container component created with connect() - reactjs

I would like to call componentDidMount() on the container component that is created by this connect()ed component:
import { View, Text } from 'react-native'
import { connect } from 'react-redux'
import { formStyles } from '../../style'
import { DimenInput } from '../dimenInput/DimenInput'
import { updateDimension } from '../../actions/updateDimension.action'
import React from 'react'
import { updateVolume } from '../../actions/updateDimension.action'
import calcVol from '../../calcVol'
const mapStateToProps = (state) => ({
height1: state.get('height').get('height1').toString(),
height2: state.get('height').get('height2').toString()
})
const updateHeight = (text, number) => (dispatch, getState) => {
dispatch(updateDimension('height', text, number))
let litres = calcVol(getState())
dispatch(updateVolume(litres))
}
let Height = (props) => (
<View style={formStyles.container}>
<DimenInput
value={props.height1}
onChangeText={text => props.updateHeight(text, 1)}
/>
<Text style={formStyles.text}>{'FT'}</Text>
<DimenInput
value={props.height2}
onChangeText={text => props.updateHeight(text, 2)}
/>
<Text style={formStyles.text}>{'IN'}</Text>
</View>
)
Height.propTypes = {
height1: React.PropTypes.string.isRequired,
height2: React.PropTypes.string.isRequired,
updateHeight: React.PropTypes.func.isRequired
}
Height = connect(
mapStateToProps,
{ updateHeight }
)(Height)
export default Height
Is it possible? I try to use connect() because it does some performance optimisations. Or do I just need to create the container component manually to add lifecycle methods?
The main aim of this is that I need to call a method on app startup. Not on component startup. So if there is another way to do that then I'm interested to know about it. There is a good way to do it with react router onEnter() however I do not have a react router as it is a single page app so no routes.

If you want access to lifecycle methods of a React Component, you'll need to use an object extended from React.Component. You can achieve this by creating your component using React.createClass or ES6 class notation instead of using a functional component like you are dong now with your arrow function.
React top level API
var Height = React.createClass({
componentDidMount: function(){
//...
}
});
ES6 approach:
class Height extends React.Component {
constructor(props) {
//...
}
componentDidMount(){
//...
}
}

Related

Access to component methods in React

I'm using a component called react-input-autosize. The issue I'm facing is that it won't resize the input on viewport resize, so I wan't to manually hook into the component methods and run copyInputStyles() and updateInputWidth().
Pretty new to React so don't know how to achieve this. You can expose the input via the inputRef, but that doesn't really help me no?
My current reduced test case looks like this, would be happy with any pointers on how to run the component methods.
import React from 'react';
import styled from '#emotion/styled';
import AutosizeInput from 'react-input-autosize';
import {throttle} from 'lodash';
const StyledAutosizeInput = styled(AutosizeInput)`
max-width: 100vw;
`;
export default class signUp extends React.Component {
constructor (props) {
super(props);
this.inputRef = React.createRef();
}
componentDidMount() {
console.log(this.inputRef); // outputs input node
window.addEventListener('resize', this.resize);
this.resize();
}
componentWillUnmount() {
window.removeEventListener('resize', this.resize);
}
resize = throttle(() => {
console.log('force resize');
}, 100);
render() {
return (
<StyledAutosizeInput
inputRef={node => this.inputRef = node}
/>
);
}
}
The inputRef={node => this.inputRef = node} callback is referring to the html input element and not the AutosizeInput component. Pass the ref via the ref prop to access the component's methods.
...
resize = throttle(() => {
if (this.inputRef.current) {
this.inputRef.current.copyInputStyles()
this.inputRef.current.updateInputWidth()
}
}, 100);
render() {
return (
<StyledAutosizeInput
ref={this.inputRef}
/>
);
}

Proper seperation of React component in container and presentational components for redux

I'm trying to seperate a component like mentioned in the title.
According to the redux tutorial for react it's a best practice to split components up.
Until now I have the following components:
ReduxTestNetwork
import React, {Component} from 'react';
import {Edge, Network, Node} from '#lifeomic/react-vis-network';
import { connect } from "react-redux";
import MyNetwork from "./MyNetwork";
...
const mapStateToProps = state => {
return { nodes: state.nodes,edges: state.edges };
};
const VisNetwork = ({nodes,edges}) => (
<MyNetwork nodes={nodes} edges={edges} options={options}/>
);
const ReduxTestNetwork = connect(mapStateToProps)(VisNetwork);
export default ReduxTestNetwork;
MyNetwork
import React, {Component} from 'react';
import {Edge, Network, Node} from '#lifeomic/react-vis-network';
import connect from "react-redux/es/connect/connect";
import {addNode} from "../actions";
const mapDispatchToProps = dispatch => {
return {
addNode: node => dispatch(addNode(node))
};
};
class MyNetwork extends Component {
constructor(props) {
super(props);
this.state = {nodes: props.nodes, edges: props.edges, options:props.options};
}
componentDidMount() {
console.log('I just mounted')
//this.onClick();
}
onClick(e){
console.log(e)
console.log(this)
/* this.props.addNode({id:5,label:'Node 5'});
this.setState(prevState => ({
nodes: [...prevState.nodes, {id:5,label:'Node 5'}]
}));*/
}
render() {
const nodes = this.state.nodes.map(node => (
<Node key={node.id} {...node}/>
));
const edges = this.state.edges.map(edge => (
<Edge key={edge.id} {...edge}/>
));
return (
<div id='network'>
<Network options={this.state.options} ref={(reduxTestNetwork) => {
window.reduxTestNetwork = reduxTestNetwork
}} onSelectNode={this.onClick.bind(this)}>
{nodes}
{edges}
</Network>
</div>);
}
}
const SVNetwork = connect(null, mapDispatchToProps)(MyNetwork);
export default SVNetwork;
I connected ReduxTestNetwork to the store to obtain the state as props and MyNetwork to be able to dispatch.
I read that presentational components should only be used to display elements and the container components should include the logic how and what to display. But I need in MyNetwork some logic also to interact with the Network component which uses a 3rd party library.
So my questions are:
Is my seperation correct?
Where should I put logic for (for example) calculating the size or color of displayed nodes?
Thanks in advance
Several things:
You don't need to use connect twice. Pass mapStateToProps and mapDispatchToProps at the same time, both on the container.
If you want to follow the path of purely presentational components, consider using a side effect library: refract, sagas, thunk... they have patterns to deal with your logic outside of the component.
If you prefer a more hand made approach, you could move every method you need to the container and pass to the component via props only the data and the function references to modify it.

Render HOC(Component) without changing Component Name in JSX

I have two HOCs that add context to a component like so :
const withContextOne = Component => class extends React.Component {
render() {
return (
<ContextOne.Consumer>
{context => <Component {...this.props} one={context} /> }
</ContextOne.Consumer>
);
}
};
export default withContextOne;
Desired Result
I just want an syntactically concise way to wrap a component with this HOC so that it doesn't impact my JSX structure too much.
What I have tried
Exporting a component with the HOC attached export default withContextOne(withContextTwo(MyComponent)) This way is the most concise, but unfortunately it breaks my unit tests.
Trying to evaluate the HOC from within JSX like :
{ withContextOne(withContextTwo(<Component />)) }
This throws me an error saying
Functions are not valid as a React child. This may happen if you return a Component instead of < Component /> from render.
Creating a variable to store the HOC component in before rendering :
const HOC = withContextOne(Component)
Then simply rendering with <HOC {...props}/> etc. I don't like this method as it changes the name of the component within my JSX
You can set the displayName before returning the wrapped component.
const withContextOne = Component => {
class WithContextOneHOC extends React.Component {
render() {
return (
<ContextOne.Consumer>
{context => <Component {...this.props} one={context} /> }
</ContextOne.Consumer>
);
}
}
WithContextOneHOC.displayName = `WithContextOneHOC(${Component.displayName})`;
return WithContextOneHOC;
};
This will put <WithContextOneHOC(YourComponentHere)> in your React tree instead of just the generic React <Component> element.
You can use decorators to ease the syntactic pain of chained HOCs. I forget which specific babel plugin you need, it might (still) be babel-plugin-transform-decorators-legacy or could be babel-plugin-transform-decorators, depending on your version of babel.
For example:
import React, { Component } from 'react';
import { withRouter } from 'react-router';
import { injectIntl } from 'react-intl';
import { connect } from 'react-redux';
import { resizeOnScroll } from './Resize';
#withRouter
#resizeOnScroll
#injectIntl
#connect(s => s, (dispatch) => ({ dispatch }))
export default class FooBar extends Component {
handleOnClick = () => {
this.props.dispatch({ type: 'LOGIN' }).then(() => {
this.props.history.push('/login');
});
}
render() {
return <button onClick={}>
{this.props.formatMessage({ id: 'some-translation' })}
</button>
}
}
However, the caveat with decorators is that testing becomes a pain. You can't use decorators with const, so if you want to export a "clean" undecorated class you're out of luck. This is what I usually do now, purely for the sake of testing:
import React, { Component } from 'react';
import { withRouter } from 'react-router';
import { injectIntl } from 'react-intl';
import { connect } from 'react-redux';
import { resizeOnScroll } from './Resize';
export class FooBarUndecorated extends Component {
handleOnClick = () => {
this.props.dispatch({ type: 'LOGIN' }).then(() => {
this.props.history.push('/login');
});
}
render() {
return <button onClick={}>
{this.props.formatMessage({ id: 'some-translation' })}
</button>
}
}
export default withRouter(
resizeOnScroll(
injectIntl(
connect(s => s, ({ dispatch }) => ({ dispatch }))(
FooBarUndecorated
)
)
)
);
// somewhere in my app
import FooBar from './FooBar';
// in a test so I don't have to use .dive().dive().dive().dive()
import { FooBarUndecorated } from 'src/components/FooBar';

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.

React Native. Getting value from ref attribute

I am trying to get value from a component but keep getting undefined refs.
Here is my code. From a function onClickSave(), I have tried to get this.refs to get a value of ref "input" in TextInputCell component but it's undefined. Is my code incorrect?
import React from 'react';
import { View, Text } from 'react-native';
import { Form, Section, TextInputCell } from 'react-native-forms';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import ActionBar3 from '../components/ActionBar3';
import * as profileActions from '../actions/profileActions';
const GLOBAL = require('../GlobalConstants');
class ProfileViewEdit extends React.Component {
constructor(props) {
super(props);
this.onClickSave.bind(this);
}
componentDidMount() {
console.log('componentDidMount');
}
onClickSave() {
console.log('aaabd');
console.log(this.refs);
}
render() {
const title = this.props.navigation.state.params.title;
let value = this.props.navigation.state.params.value;
return (
<View style={{ flex: 1, backgroundColor: '#EFEFF4' }}>
<ActionBar3
barTitle={title} navigation={this.props.navigation} onClickSave={this.onClickSave}
/>
<Section
title={title}
//helpText={'The helpText prop allows you to place text at the section bottom.'}
>
<TextInputCell
value={value}
ref="input"
/>
</Section>
</View>
);
}
}
const mapStateToProps = (state) => ({
stateProfile: state.profile
});
const mapDispatchToProps = (dispatch) => ({
actions: bindActionCreators(profileActions, dispatch)
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(ProfileViewEdit);
First thing that you are not handling events correctly. To use this in your events you need to bind this. Arrow functions bind it itself but you can bind manually to. More information is here.
You have to be careful about the meaning of this in JSX callbacks. In
JavaScript, class methods are not bound by default. If you forget to
bind this.handleClick and pass it to onClick, this will be undefined
when the function is actually called.
Second thing string refs are not suggested anymore. You should use functional ones. More info about that here.
Legacy API: String Refs
If you worked with React before, you might be familiar with an older
API where the ref attribute is a string, like "textInput", and the DOM
node is accessed as this.refs.textInput. We advise against it because
string refs have some issues, are considered legacy, and are likely to
be removed in one of the future releases. If you’re currently using
this.refs.textInput to access refs, we recommend the callback pattern
instead.
Example
<ActionBar3 barTitle={title} navigation={this.props.navigation} onClickSave={ () => this.onClickSave()} />
<TextInputCell value={value} ref={(ref) => { this.inputRef = ref; }} />

Resources