How can I update a static property in React component? - reactjs

I have a component with redux state and static property which depends on state. How can I update this static property?
import React, { Component } from 'react';
import CustomIcon from './CustomIcon';
import { connect } from 'react-redux';
import { getTranslate } from 'react-localize-redux';
class ExitButton extends Component {
static navigationOptions = {
drawerLabel: this.props.translate('exit'), // here
drawerIcon: <CustomIcon name='sign-out' size={27} withoutFeedback />
}
render() {
return null;
}
}
export default connect(
state => ({
translate: getTranslate(state.locale)
})
)(ExitButton);

You could consider creating a separate container and hooking it into the static property.
const Translator = ({ translate, text }) => translate(text)
export default connect(
state => ({
translate: getTranslate(state.locale)
})
)(Translator);
And then in your ExitButton component
...
static navigationOptions = {
drawerLabel: <Translator text='exit'>,
drawerIcon: <CustomIcon name='sign-out' size={27} withoutFeedback />
}
...
I haven't tested this but by HOC conventions, should work.

Related

React - Test separate parent component (without redux)

I wanna test parent component, but I want to do this without redux. I have problem because I've got error:
Invariant Violation: Could not find "store" in either the context or props of "Connect(MarkerList)". Either wrap the root component in a , or explicitly pass "store" as a prop to "Connect(MarkerList)".
My parent component:
export class Panel extends React.Component {
constructor(props) {
}
componentDidUpdate(prevProps) {
...
}
handleCheckBox = event => {
...
};
switchPanelStatus = bool => {
...
};
render() {
...
}
}
const mapDispatchToProps = {
isPanelSelect
};
export const PanelComponent = connect(
null,
mapDispatchToProps
)(Panel);
My child component:
export class MarkerList extends Component {
constructor(props) {
...
};
}
componentDidMount() {
...
}
componentDidUpdate() {
...
}
onSelect = (marker, id) => {
...
}
render() {
...
}
}
const mapStateToProps = state => ({
...
});
const mapDispatchToProps = {
...
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(MarkerList);
Panel test file:
import '#testing-library/jest-dom'
import "#testing-library/react"
import React from 'react'
import {render, fireEvent, screen} from '#testing-library/react'
import {Panel} from '../Panel';
test("test1", async () => {
const isPanelSelect = jest.fn();
const location = {
pathname: "/createMarker"
}
const {getByText} = render( <Panel isPanelSelect={isPanelSelect} location={location} />)
})
I've tried set store as props to Panel component or wrap It via Provider in my test file but It doesn't help me.
react-redux doesn't work without the store. You can either provide it by the context or props (usually in tests). You can provide a mock version in the test. The main problem is that both components require Redux. You have to manually forward the context to the children if it's provided as prop. The alternative solution is to mount your component within a Redux aware tree:
import { Provider } from "react-redux";
test("test1", async () => {
const { getByText } = render(
<Provider store={createFakeStore()}>
<Panel isPanelSelect={isPanelSelect} location={location} />
</Provider>
);
});

react-navigation wrap root AppContainer with react Context

I'm looking for the way to manage global state across my react-native app using react-navigation. I tried to implement basic React Context, which I wanted to wrap around the react-navigation's createAppContainer() method but it didn't work.
I ended up wrapping an app container from index.js file using Context's HOC, but it seems like react-navigation has a problem with re-rendering of nested components, when Context's state is changed. I can access my Context from nested Components but they just aren't re-rendered when context state is changed.
My index.js file looks like:
import { AppRegistry } from "react-native";
import App from "./src/App";
import { withAppContextProvider } from "./src/AppContext";
import { name as appName } from "./app.json";
AppRegistry.registerComponent(appName, () => withAppContextProvider(App));
My context class looks like:
// for new react static context API
export const AppContext = createContext({});
// create the consumer as higher order component
export const withAppContext = ChildComponent => props => (
<AppContext.Consumer>
{context => <ChildComponent {...props} global={context} />}
</AppContext.Consumer>
);
// create the Provider as higher order component (only for root Component of the application)
export const withAppContextProvider = ChildComponent => props => (
<AppContextProvider>
<ChildComponent {...props} />
</AppContextProvider>
);
export class AppContextProvider extends Component {
state = {
isOnline: true
};
handleConnectivityChange = isOnline => {
this.setState({ isOnline });
};
componentDidMount = async () => {
NetInfo.isConnected.addEventListener(
"connectionChange",
this.handleConnectivityChange
);
};
componentWillUnmount() {
NetInfo.isConnected.removeEventListener(
"connectionChange",
this.handleConnectivityChange
);
}
render() {
return (
<AppContext.Provider
value={{
...this.state
}}
>
{this.props.children}
</AppContext.Provider>
);
}
}
my App.js file looks like:
const HomeStack = createStackNavigator(
{
Home: HomeScreen,
Cities: CitiesScreen
},
getStackConfig({ initialRouteName: "Home" })
);
const SettingsStack = createStackNavigator(
{
Settings: SettingsScreen
},
getStackConfig({ initialRouteName: "Settings" })
);
export default createAppContainer(
createBottomTabNavigator(
{
Home: HomeStack,
Settings: SettingsStack
}
)
);
CitiesScreen component example:
import { AppContext } from "../AppContext";
class CitiesScreen extends Component {
static contextType = AppContext;
render() {
return (
<View style={styles.container}>
<Text>This value should change on isOnline update: {this.context.isOnline}</Text>
</View>
);
}
}
Now, when I'm accessing Context, from for example CitiesScreen component, I'm currently able to get the value of isOnline state of context but whenever I switch my internet connection (on android emulator) on/off, the context state is changed but the component isn't re-rendered and my shouldComponentUpdate() method isn't triggered. Any help to make this work?
In my case, I downgraded React from 16.8 to 16.5.0, react navigation version is 3.
I'm still investigating but that's a temporary solution for now.

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 to get the theme outside styled-components?

I know how to get the theme from components that are created using the styled way:
const StyledView = styled.View`
color: ${({ theme }) => theme.color};
`;
But how to get from normal components or apply it for different properties? Example:
index.js
<ThemeProvider theme={{ color: 'red' }}>
<Main />
</ThemeProvider>
main.js
<View>
<Card aCustomColorProperty={GET COLOR FROM THEME HERE} />
</View>
Notice how the property that needs the theme is not called style
You can use the useTheme hook since v5.0:
import React, { useTheme } from 'styled-components';
export function MyComponent() {
const theme = useTheme();
return <p style={{ color: theme.color }}>Text</p>;
}
You can also use the withTheme higher order component that I contributed a long time ago since v1.2:
import { withTheme } from 'styled-components'
class MyComponent extends React.Component {
render() {
const { theme } = this.props
console.log('Current theme: ', theme);
// ...
}
}
export default withTheme(MyComponent)
original response below (ignore this!)
While there is no official solution, I came up by now:
Create a Higher Order Component that will be responsable to get the current theme and pass as a prop to a component:
import React from 'react';
import { CHANNEL } from 'styled-components/lib/models/ThemeProvider';
export default Component => class extends React.Component {
static contextTypes = {
[CHANNEL]: React.PropTypes.func,
};
state = {
theme: undefined,
};
componentWillMount() {
const subscribe = this.context[CHANNEL];
this.unsubscribe = subscribe(theme => {
this.setState({ theme })
});
}
componentWillUnmount() {
if (typeof this.unsubscribe === 'function') this.unsubscribe();
}
render() {
const { theme } = this.state;
return <Component theme={theme} {...this.props} />
}
}
Then, call it on the component you need to access the theme:
import Themable from './Themable.js'
const Component = ({ theme }) => <Card color={theme.color} />
export default Themable(Component);
You can use useTheme hook
import { useTheme } from 'styled-components';
const ExampleComponent = () => {
const theme = useTheme();
return (
<View>
<Card aCustomColorProperty={theme.color.sampleColor} />
</View>
);
};
Creating a HOC is a good way to tackle theming. Let me share another idea using React's Context.
Context allows you to pass data from a parent node to all it’s children.
Each child may choose to get access to context by defining contextTypes in the component definition.
Let's say App.js is your root.
import themingConfig from 'config/themes';
import i18nConfig from 'config/themes';
import ChildComponent from './ChildComponent';
import AnotherChild from './AnotherChild';
class App extends React.Component {
getChildContext() {
return {
theme: themingConfig,
i18n: i18nConfig, // I am just showing another common use case of context
}
}
render() {
return (
<View>
<ChildComponent />
<AnotherChild myText="hola world" />
</View>
);
}
}
App.childContextTypes = {
theme: React.PropTypes.object,
i18n: React.PropTypes.object
};
export default App;
Now our `ChildComponent.js who wants some theme and i18n strings
class ChildComponent extends React.Component {
render() {
const { i18n, theme } = this.context;
return (
<View style={theme.textBox}>
<Text style={theme.baseText}>
{i18n.someText}
</Text>
</View>
);
}
}
ChildComponent.contextTypes = {
theme: React.PropTypes.object,
i18n: React.PropTypes.object
};
export default ChildComponent;
AnotherChild.js who only wants theme but not i18n. He might be stateless as well:
const AnotherChild = (props, context) {
const { theme } = this.context;
return (<Text style={theme.baseText}>{props.myText}</Text>);
}
AnotherChild.propTypes = {
myText: React.PropTypes.string
};
AnotherChild.contextTypes = {
theme: React.PropTypes.object
};
export default AnotherChild;
To use withTheme in a functional component create a Higher-order-component.
Higher-order-component:
higher-order components, or HOCs, are functions that take a component and output a new component after enhancing it in some manner:
const EnhancedHOCComponent = hoc(OriginalReactComponent)
Sample withTheme in a functional Component
const MyButton = ({theme}) => {
const red = theme.colors.red;
return (<div style={{ color: red}} >how are you</div>)
}`
const Button = withTheme(MyButton);
export default Button;

Decoupling React Components and Redux Connect

As seen here I am trying to decouple my app's components as much as I can and make them not aware of any storage or action creator.
The goal is to have them to manage their own state and call functions to emit a change. I have been told that you do this using props.
Considering
// Menu.jsx
import React from 'react'
import { className } from './menu.scss'
import Search from 'components/search'
class Menu extends React.Component {
render () {
return (
<div className={className}>
<a href='#/'>Home</a>
<a href='#/foo'>foo</a>
<a href='#/bar'>bar</a>
<Search />
</div>
)
}
}
And
// Search.jsx
import React from 'react'
import { className } from './search.scss'
class Search extends React.Component {
render () {
let { searchTerm, onSearch } = this.props
return (
<div className={`search ${className}`}>
<p>{searchTerm}</p>
<input
type='search'
onChange={(e) => onSearch(e.target.value)}
value={searchTerm}
/>
</div>
)
}
}
Search.propTypes = {
searchTerm: React.PropTypes.string,
onSearch: React.PropTypes.function
}
export default Search
And reading here I see a smart use of Provider and connect and my implementation would look something like this:
import { bindActionCreators, connect } from 'redux'
import actions from 'actions'
function mapStateToProps (state) {
return {
searchTerm: state.searchTerm
}
}
function mapDispatchToProps (dispatch) {
return bindActionCreators({
dispatchSearchAction: actions.search
}, dispatch)
}
export default connect(mapStateToProps, mapDispatchToProps)(Search)
Assuming I have a store handling searchTerm as part of the global state.
Problem is, where does this code belongs to? If I put it in Search.jsx I will couple actions with the component and more important to redux.
Am I supposed to have two different versions of my component, one decoupled and one connect()ed and have <Menu /> to use it? If yes what would my files tree look like? One file per component or a like a make-all-connected.js ?
In redux, exist a new kind of component that is called containers, this is the component that use connect(mapStateToProps, mapActionsToProps), to pass the state and actions to the current component.
All depends of the use of the component. For example, if you component Search only going to be use with the same state and action, You container could be the same that your component like this:
// Search.jsx
import { connect } from 'redux'
import actions from 'actions'
import React from 'react'
import { className } from './search.scss'
class Search extends React.Component {
render () {
let { searchTerm, onSearch } = this.props
return (
<div className={`search ${className}`}>
<p>{searchTerm}</p>
<input
type='search'
onChange={(e) => onSearch(e.target.value)}
value={searchTerm}
/>
</div>
)
}
}
Search.propTypes = {
searchTerm: React.PropTypes.string,
onSearch: React.PropTypes.function
}
function mapStateToProps ({searchTerm}) {
return {
searchTerm
};
}
const mapDispatchToProps = {
onSearch: actions.search
}
export default connect(mapStateToProps, mapDispatchToProps)(Search)
But if your plan is reuse this component in another containers and the searchTerm or the action are different on the global state. The best way is passing this properties through other containers, and keep the Search component pure. Like this:
// Container1.jsx
import { connect } from 'redux'
import actions from 'actions'
import React, { Component } from 'react'
class Container1 extends Component {
render() {
const { searchTerm, handleOnSearch } = this.props;
return (
<div>
<Search searchTerm={searchTerm} onSearch={handleOnSearch} />
</div>
)
}
}
function mapStateToProps ({interState: {searchTerm}}) {
return {
searchTerm
};
}
const mapDispatchToProps = {
handleOnSearch: actions.search
}
export default connect(mapStateToProps, mapDispatchToProps)(Container1)
// Container2.jsx
import { connect } from 'redux'
import otherActions from 'otheractions'
import React, { Component } from 'react'
class Container2 extends Component {
render() {
const { searchTerm, handleOnSearch } = this.props;
return (
<div>
<Search searchTerm={searchTerm} onSearch={handleOnSearch} />
</div>
)
}
}
function mapStateToProps ({otherState: {searchTerm}}) {
return {
searchTerm
};
}
const mapDispatchToProps = {
handleOnSearch: otherActions.search
}
export default connect(mapStateToProps, mapDispatchToProps)(Container2)
For more information, read the official docs about using redux with react.

Resources