How to set component state with context.state? - reactjs

I recently ran into an issue with React Context.
I have some data stored in localstorage which I only intend to use when there is no data available from the context provider.
The data from localstorage is stored in my component's state.
I would like to override this.state if there is available data coming from context.
The struggle is that I don't know how to set the state when the context can only be used in the render method.
It is a very bad practice to call a setState in the render and I have no idea how to get the context.state outside of the render.
If there is no data from
There is some sample code below.
Any ideas are welcome which are taking me closer to the solution.
constructor(props) {
super(props);
this.state = {
data: ''
}
}
componentDidMount() {
this.setState({
data: localstorage.getItem('data')
})
}
render() {
return (
<>
<AppContext.Consumer>
{context => (
<>
{typeof context.state.data !== 'undefined'&&
<div>
{/*Print out data from this.state or from context.state*/}
</div>
}
</>
)}
</AppContext.Consumer>
</>
)
}

You can access to context outside render with following trick:
import { PageTitleContext } from '../lib/pageTitleProvider';
import { Helmet } from 'react-helmet';
import * as PropTypes from 'prop-types';
class PageTitle extends Component {
componentDidMount() {
if (this.props.title)
this.props.context.setTitle(this.props.title);
}
componentDidUpdate(prevProps, prevState, snapshot) {
if (this.props.title !== prevProps.title)
this.props.context.setTitle(this.props.title);
}
render() {
if (this.props.title) {
return <Helmet>
<title>{this.props.context.title}</title>
</Helmet>;
}
if (!this.props.title) {
return this.props.context.title;
}
}
}
PageTitle.propTypes = {
title: PropTypes.string,
context: PropTypes.object.isRequired,
};
export default (props) => (
<PageTitleContext.Consumer>
{(context) => <PageTitle {...props} context={context}/>}
</PageTitleContext.Consumer>
)

Related

Modify react child components state for storybook

I have a react Component that I am trying to add to a storybook. It has child components that have component state which changes the way the component displays. I would like to set that component state to show in my storybook. What is the best way to achieve this ?
class ParentComponent extends PureComponent<ParentComponentProps> {
render() {
return (
<ChildComponent />
)
}
}
class ChildComponent extends PureComponent<ChildComponentProps> {
constructor(props) {
super(props);
this.handleOnBlur = this.handleOnBlur.bind(this);
this.state = {
isValid: true
};
}
handleOnBlur() {
this.setState({
isValid: isInputValid()
});
}
render() {
return (
<TextField
placeholder="eg. 12345"
validationMessage={'not a valid input'}
isInvalid={this.state.isValid}
onBlur={this.handleOnBlur}
/>
)
}
}
And Storybook code looks like this at the moment
import React from 'react';
import { Provider } from 'react-redux';
import ParentComponent from './ParentComponent';
export default { title: 'UpdateChildComponent' };
export const FieldValidationShowing = (state) => {
const { store, updateState } = mockStore;
updateState(state);
return (
<Provider store={store}>
<ParentComponent />
</Provider>
);
};
The above code is a sample of what I am doing.

React native context api not updated

I'm using RN NetInfo to check if user connected to internet using component <NetworkProvider /> and I want to pass this components stats to all screens and components in my app.
The problem is context api works good when I use it inside render function but when I use inside componentDidMount or componentWillMount the state not changed. Return initial value of isConnected state.
Please read comment in code
so this my code
NetworkProvider.js
import React,{PureComponent} from 'react';
import NetInfo from '#react-native-community/netinfo';
export const NetworkContext = React.createContext({ isConnected: true });
export class NetworkProvider extends React.PureComponent {
state = {
isConnected: true // initial value
};
componentDidMount() {
NetInfo.isConnected.addEventListener('connectionChange', this.handleConnectivityChange);
}
componentWillUnmount() {
NetInfo.isConnected.removeEventListener('connectionChange', this.handleConnectivityChange);
}
handleConnectivityChange = isConnected => this.setState({ isConnected });
render() {
return (
<NetworkContext.Provider value={this.state}>
{this.props.children}
</NetworkContext.Provider>
);
}
}
this index.js
...
import { NetworkContext } from '../components/NetworkProvider';
export default class index extends Component {
static navigationOptions = {};
static contextType = NetworkContext;
constructor(props) {
super(props);
this.state = {
...
};
}
componentDidMount() {
// return object state but with inital value {isConnected :true}
console.log(this.context);
//this.fetchData(this.state.page);
}
render() {
// here when I use this.context return object {isConnected:true/false} depends on internet connection status on device
return(
<FlatList
...
/>
)
}
}
...

React update child's props after parent's state change

I'm trying to pass Draft.js's editor state from the editor component to my own Sidebar component.
Using the topmost component Notes I use a callback to get the editor state from CustomEditor and set it as the Notes state. I then pass that state to Sidebar as a prop.
The problem is that the prop is set before the callback fires. I was thinking a setTimeout but that seems rough. I'm aware of UNSAFE_componentWillReceiveProps() but the docs don't recommend it. Is there something in react for this use case?
export class Notes extends Component {
constructor(props) {
super(props);
this.getEditorState = this.getEditorState.bind(this)
this.state = {
editorState: "the placeholder data Sidebar should not have as a prop"
};
}
getEditorState(state) {
console.log(state)
this.setState({editorState: state})
}
render() {
return (
<section id="Notes">
<div id="editor-holder">
<Sidebar currentEditorState={this.state.editorState}/>
<div id="Editor">
<FileHeader />
<CustomEditor getState={this.getEditorState}/>
</div>
</div>
</section>
);
}
}
export default Notes;
The new Context API is the solution to this type of problem. Took a bit to get my head around it, but what I came up with gets editorState to Sidebar as a prop.
export const NoteContext = React.createContext("placeholderEditorState");
export class Notes extends Component {
constructor(props) {
super(props);
this.getEditorState = this.getEditorState.bind(this)
this.getFolderData = this.getFolderData.bind(this)
this.state = {
editorState: null,
folderData: null
};
}
getEditorState(state) {
this.setState({editorState: state});
}
getFolderData(data) {
this.setState({folderData : data})
}
render() {
return (
<section id="Notes">
<TopBar />
<div id="editor-holder">
<NoteContext.Provider value={{editorState: this.state.editorState}} >
<NoteContext.Consumer>
{(context)=>{ return (
<Sidebar currentEditorState={context.editorState} getFolderData={this.getFolderData}/>
)}}
</NoteContext.Consumer>
</NoteContext.Provider>
<div id="Editor">
<NoteContext.Provider value={{folderData: this.state.folderData}} >
<FileHeader />
</NoteContext.Provider>
<CustomEditor getState={this.getEditorState}/>
</div>
</div>
</section>
);
}
}
Looking at it now it seems very straightforward, that means I've learnt a lot! Let me know if I can improve anything here.
Well there are more possible options how to achieve this result
Conditional rendering
You can render <Sidebar> only when props has altered that menas
constructor(props)
super(props)
this.state = {
editorState: false
}
}
render() {
... {this.state.editorState && <Sidebar currentEditorState={this.state.editorState}/>}
}
Guard component for undefined/false props
Sidebar.js
render() {
if(!this.props.currentEditorState) return null // this will force React to render nothing
return ( ... )
}
Transition props to state with getDerivedState
https://reactjs.org/docs/react-component.html#static-getderivedstatefromprops
Sidebar.js
static getDerivedStateFromProps({ currentEditorState }, prevState) {
if(currentEditorState !== false {
return { currentEditorState }
} else {
return {}
}
}
render() {
(.... something bound to this.state.currentEditorState)
}
Use context (legacy context)
class Notes extends React.Component {
getEditorState(state) {
console.log(state)
this.setState({editorState: state})
}
getChildContext() {
return {
editorState: this.state.editorState
}
}
childContextTypes = {
editorState: PropTypes.oneOfType([PropTypes.obj, PropTypes.bool])
}
}
Sidebar.js
class Sidebar {
static contextTypes = {
editorState: PropTypes.oneOfType([PropTypes.obj, PropTypes.bool])
}
render() {
... this.context.editorState
}
}

Remove a React component from the DOM

I have this piece of code (which I've simplified for posting here) that creates a component and renders it
const getComponentToRender = (user, path) => {
switch(path ){
case 'ChangePassword':
return <ChangePassword user={ user } />;
case 'NewPassword':
return <NewPassword user={ user } />;
case 'PasswordExpire':
return <PasswordExpire user={ user } />;
default:
return null;
}
}
class UserAdmin extends React.Component {
static propTypes = {
user: PropTypes.object.isRequired
};
render() {
const component = getComponentToRender(this.props.user, 'ChangePassword' );
return(
<div id='user-admin-wrapper'>
{component}
</div>
)
}
componentWillUnmount(){
}
}
When I navigate away from UserAdmin the componentWillUnmount gets called.
Q: What is the simplest way to actually remove the component ChangePassword or any other component (by its name) from the DOM when componentWillUnmount executes.
OR, removing the component at any point, without waiting for componentWillUnmount
Using react-dom 15.6.1 . btw
Un-mounting a component will un-mount(remove) all the child components it contains. So after componentWillUnmount the component you rendered inside it will be removed.
If you need to control over components that rendered without un-mounting you use conditional render logic.
Example
class SomeComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
shouldIRender: true
};
}
componentDidMount() {
setTimeout(() => {
this.setState({shouldIRender: false});
}, 5000);
}
render() {
return(
<div>
<ComponentThatAlwaysHere />
{ this.state.shouldIRender === true ? <ComponentThatRemovesAfterStateChange /> : null }
{ this.state.shouldIRender === true && <AnotherComponentThatRemovesAfterStateChange /> }
</div>
)
}
}

forceUpdate is not re-rendering children

I'm using the react, redux react-router stack for my webapp. In the top level component's(the component that renders on the root path) componentDidMount I'm subscribing to the store as shown below
import NotificationsList from './components/notifier';
import React from 'react';
let Spinner = ({
isVisible,
showSpinner,
solidBackdrop
}) => (
<div style={{opacity: solidBackdrop ? 1 : 0.5}} className={"spinner " + (isVisible ? '' : 'hide')}></div>
);
export default class AppPage extends React.Component {
static contextTypes = {
store: React.PropTypes.object,
router: React.PropTypes.object
};
handleDismissNotification(notification) {
this.context.store.dispatch({
type: 'REMOVE_NOTIFICATION',
data: notification
});
}
componentDidMount() {
this.context.store.subscribe(() => this.forceUpdate());
}
render() {
let state = this.context.store.getState();
let props = {
notifications: state.notifications,
handleDismiss: this.handleDismissNotification.bind(this)
};
return (
<div className="'apppage-container">
{this.props.children}
<NotificationsList {...props} />
<Spinner isVisible={state.initialFetchInProgress || state.requestInProgress}
showSpinner={!state.initialFetchInProgress} solidBackdrop={state.initialFetchInProgress}/>
</div>
);
}
}
this.props.children here renders the component shown below
import Header from './components/header';
import React from 'react';
class ContentPage extends React.Component {
static contextTypes = {
store: React.PropTypes.object
};
render() {
let user = this.context.store.getState().user;
return <div className="content-container">
<Header user/>
</div>
}
}
export default ContentPage;
The problem is that when the first time a render happens, everything goes fine. Then when the render happens through forceUpdate, the child component is not getting re-rendered.
I think I got it. Every container component should be subscribed to the store separately. So accordingly, ContentPage should also have
componentDidMount() {
this.context.store.subscribe(() => this.forceUpdate());
}
As you replied to yourself, indeed the container component should subscribe to the store , but in addition to the subscription, it's good practice for the the container to also unsubscribe when unmounted :
componentDidMount() {
this.unsubscribe = this.context.store.subscribe(() => this.forceUpdate());
}
componentWillUnmount() {
this.unsubscribe();
}

Resources