I have a component providing a context (=provider) and some children components are consuming the context.
My provider also wraps an ErrorBoundary React component in case of a children crash. From there, if an error happens, the ErrorBoundary React component is updating some values in my context.
But it looks like the value is not update and I don't know why.
Here how my code looks like:
const MyContext = React.createContext<{
state: FsState;
setState: Dispatch<SetStateAction<FsState>> | null;
}>({ state: { ...initState }, setState: null });
const [state, setState] = useState({ ...initState });
const handleError = (error: Error): void => {
setState({
...state,
status: {
...state.status,
hasError: !!error
}
});
};
return (
<MyContext.Provider value={{ state, setState }}>
<ErrorBoundary
onError={handleError}
>
{children}
</ErrorBoundary>
</MyContext.Provider>
);
ErrorBoundary component:
class ErrorBoundary extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { error: null, errorInfo: null };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
this.props.onError(error);
this.setState({
error,
errorInfo
});
}
render(): React.ReactNode {
const { error, errorInfo } = this.state;
const { children } = this.props;
if (errorInfo) {
return (
<>
<div
>
<h1>
Something went wrong.
</h1>
</div>
<div>{children}</div>
</>
);
}
return children;
}
}
And a child component (of my Provider + ErrorBoundary)
export const ChildElement = () => {
const {
state: { status },
} = useContext(MyContext);
if (status.hasError) {
return <div>I crashed but I'm in safe mode</div>;
}
throw new Error("test test"); // trigger an error
return <div>I'll never work</div>;
};
I never enter inside the if loop despite the fact that when debugging, the componentDidCatch is correctly triggered.
Thanks to your playground #Shlang, I found the issue.
This was indeed something deeper. I have another child component having a useEffect and using setState as well to update some other values.
The thing was, it was overriding the hasError:true back to hasError:false because for some asynchronous reason, it was executed after the error is caught by the ErrorBoundary.
I did a workaround by moving the hasError attribute in another useState hook.
Thank you !
Related
I have my react app created from vite and there I have my Custom React Error Boundary Component wrap from Components the thing is it cannot catch errors.i debug my error component but it cannot recieve any value in getDerivedStateFromError not componentDidCatch
Here is my Error Boundary Code:
/* eslint-disable #typescript-eslint/no-unused-vars */
import React, { Component } from 'react';
interface IState {
hasError: boolean;
eventId?: string;
}
// eslint-disable-next-line #typescript-eslint/no-empty-interface
interface IProps {
children: any;
}
export default class ErrorBoundary extends Component<IProps, IState> {
constructor(props: Readonly<{}>) {
super(props);
this.state = { eventId: '', hasError: false };
}
static getDerivedStateFromError(error: any) {
console.log('here get Derived');
// Update state so the next render will show the fallback UI.
return { hasError: true };
}
componentDidCatch(error: any, errorInfo: any) {
console.log('My errors', error);
}
render() {
// const { children } = this.props;
console.log('errors');
if (this.state.hasError) {
console.log('errors found', this.state.hasError);
return (
<button
onClick={() =>
console.log("Error Found)
}
>
Report feedback
</button>
);
}
return this.props.children;
}
}
and my app.js code:
import './App.css';
function App() {
return (
<div className="App">
<header className="App-header">
<ErrorBoundary>
<button
onClick={() => {
throw new Error('Im new Error');
}}
>
Click Me
</button>
</ErrorBoundary>
</header>
</div>
);
}
export default App;
does anyone knows what is the issue ?
Error boundaries do not catch errors for:
Event handlers
Asynchronous code (e.g. setTimeout or requestAnimationFrame
callbacks)
Server side rendering
Errors thrown in the error boundary itself
(rather than its children)
https://reactjs.org/docs/error-boundaries.html#introducing-error-boundaries
To simulate an error, you need to create a component, make it a child of ErrorBoundary class and click on the button 2 times
function Button() {
const [count, setCount] = useState(0);
const onClick = () => {
setCount(count + 1);
};
useEffect(() => {
if (count === 2) {
throw new Error('I crashed!');
}
});
return (
<button
onClick={onClick}
>
Click Me
</button>
);
}
export default Button;
I developed my first React component that was Function based and I'm now trying to refactor it to be Class based. However, I can't seem to get it to work when trying to convert it over. I'm pretty sure that the issue is with the RenderItem method, and when I try to bind it, I get this error: TypeError: Cannot read property 'bind' of undefined. How can I bind a method that's a child of a parent method? Is this possible, and if not what would be a better solution?
Error when compiling:
Line 35:10: 'state' is assigned a value but never used no-unused-vars
Line 51:9: 'renderItem' is assigned a value but never used no-unused-vars
import React, { useEffect, useReducer } from 'react';
import API from '#aws-amplify/api';
import { List } from 'antd';
import { listQuestions as ListQuestions } from '../../graphql/queries';
export default class QuestionLoader extends React.Component {
state = {
questions: [],
loading: true,
error: false,
form: { asked: '', owner: '' },
};
constructor() {
super();
this.GetQuestion = this.GetQuestion.bind(this);
this.reducer = this.reducer.bind(this);
// this.renderItem = this.renderItem.bind(this);
console.log('constructor', this);
}
reducer(state, action) {
switch (action.type) {
case 'SET_QUESTIONS':
return { ...state, questions: action.questions, loading: false };
case 'ERROR':
return { ...state, loading: false, error: true };
default:
return state;
}
}
GetQuestion() {
const [state, dispatch] = useReducer(this.reducer, this.state);
useEffect(() => {
fetchQuestions();
}, []);
async function fetchQuestions() {
try {
const questionData = await API.graphql({ query: ListQuestions });
dispatch({ type: 'SET_QUESTIONS', questions: questionData.data.listQuestions.items });
} catch (err) {
console.log('error: ', err);
dispatch({ type: 'ERROR' });
}
}
const renderItem = (item) => {
console.log(this);
return <List.Item.Meta title={item.asked} description={item.owner} />;
};
}
render() {
return (
<div>
<List loading={this.state.loading} dataSource={this.state.questions} renderItem={this.renderItem} />
</div>
);
}
}
// export default QuestionLoader;
you cannot mix functional component and class component thing.
useEffect, useReducer are wrong one to use with class component.
Don't use bind, use arrow function to create the method. Remove constructor.
import React from 'react';
export default class QuestionLoader extends React.Component {
state = {
data: "name"
};
handleClick = () => {
this.setState({
name:"test"
})
}
render() {
return (<div> {this.state.name}
<button onClick={this.handleClick}></button>
</div>
}
}
I am new in react.I am trying to use react-redux style from the beginning.
Below is what I tried for a simple product listing page.
In my App.js for checking if the user is still logged in.
class App extends Component {
constructor(props) {
super(props);
this.state = {}
}
componentDidMount() {
if (isUserAuthenticated() === true) {
const token = window.localStorage.getItem('jwt');
if (token) {
agent.setToken(token);
}
this.props.appLoad(token ? token : null, this.props.history);
}
}
render() {
const PrivateRoute = ({ component: Component, ...rest }) => (
<Route {...rest} render={(props) => (
isUserAuthenticated() === true
? <Component {...props} />
: <Redirect to='/logout' />
)} />
)
return (
<React.Fragment>
<Router>
<Switch>
{routes.map((route, idx) =>
route.ispublic ?
<Route path={route.path} component={withLayout(route.component)} key={idx} />
:
<PrivateRoute path={route.path} component={withLayout(route.component)} key={idx} />
)}
</Switch>
</Router>
</React.Fragment>
);
}
}
export default withRouter(connect(mapStatetoProps, { appLoad })(App));
In my action.js appLoaded action is as under
export const appLoad = (token, history) => {
return {
type: APP_LOAD,
payload: { token, history }
}
}
reducer.js for it
import { APP_LOAD, APP_LOADED, APP_UNLOADED, VARIFICATION_FAILED } from './actionTypes';
const initialState = {
appName: 'Etsync',
token: null,
user: null,
is_logged_in: false
}
const checkLogin = (state = initialState, action) => {
switch (action.type) {
case APP_LOAD:
state = {
...state,
user: action.payload,
is_logged_in: false
}
break;
case APP_LOADED:
state = {
...state,
user: action.payload.user,
token: action.payload.user.token,
is_logged_in: true
}
break;
case APP_UNLOADED:
state = initialState
break;
case VARIFICATION_FAILED:
state = {
...state,
user: null,
}
break;
default:
state = { ...state };
break;
}
return state;
}
export default checkLogin;
And in Saga.js I have watched every appLoad action and performed the operation as under
import { takeEvery, fork, put, all, call } from 'redux-saga/effects';
import { APP_LOAD } from './actionTypes';
import { appLoaded, tokenVerificationFailed } from './actions';
import { unsetLoggeedInUser } from '../../helpers/authUtils';
import agent from '../agent';
function* checkLogin({ payload: { token, history } }) {
try {
let response = yield call(agent.Auth.current, token);
yield put(appLoaded(response));
} catch (error) {
if (error.message) {
unsetLoggeedInUser();
yield put(tokenVerificationFailed());
history.push('/login');
} else if (error.response.text === 'Unauthorized') {
unsetLoggeedInUser();
yield put(tokenVerificationFailed());
}
}
}
export function* watchUserLogin() {
yield takeEvery(APP_LOAD, checkLogin)
}
function* commonSaga() {
yield all([fork(watchUserLogin)]);
}
export default commonSaga;
After that for productLists page my code is as under
//importing part
class EcommerceProductEdit extends Component {
constructor(props) {
super(props);
this.state = {}
}
componentDidMount() {
**//seeing the props changes**
console.log(this.props);
this.props.activateAuthLayout();
if (this.props.user !== null && this.props.user.shop_id)
this.props.onLoad({
payload: Promise.all([
agent.Products.get(this.props.user),
])
});
}
render() {
return (
// JSX code removed for making code shorter
);
}
}
const mapStatetoProps = state => {
const { user, is_logged_in } = state.Common;
const { products } = state.Products.products.then(products => {
return products;
});
return { user, is_logged_in, products };
}
export default connect(mapStatetoProps, { activateAuthLayout, onLoad })(EcommerceProductEdit);
But in this page in componentDidMount if I log the props, I get it three time in the console. as under
Rest everything is working fine. I am just concerned,the code i am doing is not up to the mark.
Any kinds of insights are highly appreciated.
Thanks
It's because you have three state updates happening in ways that can't batch the render.
You first render with no data. You can see this in the first log. There is no user, and they are not logged in.
Then you get a user. You can see this in the second log. There is a user, but they are not logged in.
Then you log them in. You can see this in the third log. There is a user, and they are logged in.
If these are all being done in separate steps and update the Redux store each step you'll render in between each step. If you however got the user, and logged them in, and then stored them in the redux state in the same time frame you'd only render an additional time. Remember React and Redux are heavily Async libraries that try to use batching to make sure things done in the same time frame only cause one render, but sometimes you have multiple network steps that need to be processed at the same time. So no you're not doing anything wrong, you just have a lot of steps that can't easily be put into the same frame because they rely on some outside resource that has its own async fetch.
I have a component parent and a component child with some props connected to the parent state.
In the parent I call setState but the componentWillReceiveProps function of the child is not fired.
More precisaly, its fired in a certain point of the parent, its not fired in another point.
This is the parent:
... imports
class HomeScreen extends Component {
constructor(props) {
super(props);
dispatchFbPermissionAction = this.dispatchFbPermissionAction.bind(this);
this.state = {
fbPermissions: [],
}
}
componentDidMount () {
this._loadInitialState();
}
_responsePermissionsCallback(error: ?Object, result: ?Object) {
if (error) {
log('Error fetching data: ' + error.toString());
} else {
dispatchFbPermissionAction(result.data);
}
}
dispatchFbPermissionAction = (data) => {
// **NOT FIRED**
this.setState({
fbPermissions: data
});
this.props.fbPermissionsLoaded(data);
}
async _loadInitialState() {
AccessToken.getCurrentAccessToken().then(
(data) => {
if (data) {
const infoRequest = new GraphRequest(
'/me/permissions',
null,
this._responsePermissionsCallback,
);
new GraphRequestManager().addRequest(infoRequest).start();
// **FIRED**
this.setState({
...
});
this.props.loggedIn();
}
}
);
}
render () {
const { navigation } = this.props;
return (
<Container>
<ScrollableTabView
<View tabLabel="ios-film" style={styles.tabView}>
<Text style={styles.tabTitle}>{_.toUpper(strings("main.theatres"))}</Text>
<ListTheatre navigation={this.props.navigation} filterText={this.state.filterText} isLoggedIn={this.state.isLoggedIn} fbPermissions={this.state.fbPermissions}></ListTheatre>
</View>
</ScrollableTabView>
</Container>
)
}
}
const mapStateToProps = (state) => {
return {
isLoggedIn: state.isLoggedIn,
listTheatre: state.listTheatre,
listMusic: state.listMusic
};
};
// wraps dispatch to create nicer functions to call within our component
const mapDispatchToProps = (dispatch) => ({
startup: () => dispatch(StartupActions.startup()),
loggedIn: () => dispatch({
type: LOGGED_IN
}),
fbPermissionsLoaded: (data) => dispatch({
type: FB_PERMISSIONS_LOADED,
fbPermissions: data
})
});
export default connect(mapStateToProps, mapDispatchToProps)(HomeScreen)
And this is the child:
... imports
class ListTheatre extends Component {
constructor(props) {
super(props);
this.state = {
...
}
}
componentWillReceiveProps(nextProps) {
log(this.props)
}
shouldComponentUpdate(nextProps, nextState) {
return !nextState.fetching;
}
render() {
const { navigate } = this.props.navigation;
return (
<SectionList
...
/>
)
}
}
ListTheatre.propTypes = {
isLoggedIn: PropTypes.bool.isRequired,
}
const mapStateToProps = (state) => {
return {
isLoggedIn: state.isLoggedIn
};
};
const mapDispatchToProps = (dispatch) => ({
startup: () => dispatch(StartupActions.startup())
});
export default connect(mapStateToProps, mapDispatchToProps)(ListTheatre);
I do not why the setState after the GraphRequestManager().addRequest call works like a charm (the componentWillReceiveProps function of the child is fired), while the setState in the dispatchFbPermissionAction function does not fire the componentWillReceiveProps function of the child.
This is due to connect/Connect(ListTheatre) that wraps your ListTheatre component implemented sCU(shouldComponentUpdate) internally for you, turn it off by setting pure option of connect to false like
export default connect(mapStateToProps, mapDispatchToProps, null, {pure: false})(ListTheatre)
[pure] (Boolean): If true, connect() will avoid re-renders and calls to mapStateToProps, mapDispatchToProps, and mergeProps if the relevant state/props objects remain equal based on their respective equality checks. Assumes that the wrapped component is a “pure” component and does not rely on any input or state other than its props and the selected Redux store’s state. Default value: true
I have updated this with an update at the bottom
Is there a way to maintain a monolithic root state (like Redux) with multiple Context API Consumers working on their own part of their Provider value without triggering a re-render on every isolated change?
Having already read through this related question and tried some variations to test out some of the insights provided there, I am still confused about how to avoid re-renders.
Complete code is below and online here: https://codesandbox.io/s/504qzw02nl
The issue is that according to devtools, every component sees an "update" (a re-render), even though SectionB is the only component that sees any render changes and even though b is the only part of the state tree that changes. I've tried this with functional components and with PureComponent and see the same render thrashing.
Because nothing is being passed as props (at the component level) I can't see how to detect or prevent this. In this case, I am passing the entire app state into the provider, but I've also tried passing in fragments of the state tree and see the same problem. Clearly, I am doing something very wrong.
import React, { Component, createContext } from 'react';
const defaultState = {
a: { x: 1, y: 2, z: 3 },
b: { x: 4, y: 5, z: 6 },
incrementBX: () => { }
};
let Context = createContext(defaultState);
class App extends Component {
constructor(...args) {
super(...args);
this.state = {
...defaultState,
incrementBX: this.incrementBX.bind(this)
}
}
incrementBX() {
let { b } = this.state;
let newB = { ...b, x: b.x + 1 };
this.setState({ b: newB });
}
render() {
return (
<Context.Provider value={this.state}>
<SectionA />
<SectionB />
<SectionC />
</Context.Provider>
);
}
}
export default App;
class SectionA extends Component {
render() {
return (<Context.Consumer>{
({ a }) => <div>{a.x}</div>
}</Context.Consumer>);
}
}
class SectionB extends Component {
render() {
return (<Context.Consumer>{
({ b }) => <div>{b.x}</div>
}</Context.Consumer>);
}
}
class SectionC extends Component {
render() {
return (<Context.Consumer>{
({ incrementBX }) => <button onClick={incrementBX}>Increment a x</button>
}</Context.Consumer>);
}
}
Edit: I understand that there may be a bug in the way react-devtools detects or displays re-renders. I've expanded on my code above in a way that displays the problem. I now cannot tell if what I am doing is actually causing re-renders or not. Based on what I've read from Dan Abramov, I think I'm using Provider and Consumer correctly, but I cannot definitively tell if that's true. I welcome any insights.
There are some ways to avoid re-renders, also make your state management "redux-like". I will show you how I've been doing, it far from being a redux, because redux offer so many functionalities that aren't so trivial to implement, like the ability to dispatch actions to any reducer from any actions or the combineReducers and so many others.
Create your reducer
export const initialState = {
...
};
export const reducer = (state, action) => {
...
};
Create your ContextProvider component
export const AppContext = React.createContext({someDefaultValue})
export function ContextProvider(props) {
const [state, dispatch] = useReducer(reducer, initialState)
const context = {
someValue: state.someValue,
someOtherValue: state.someOtherValue,
setSomeValue: input => dispatch('something'),
}
return (
<AppContext.Provider value={context}>
{props.children}
</AppContext.Provider>
);
}
Use your ContextProvider at top level of your App, or where you want it
function App(props) {
...
return(
<AppContext>
...
</AppContext>
)
}
Write components as pure functional component
This way they will only re-render when those specific dependencies update with new values
const MyComponent = React.memo(({
somePropFromContext,
setSomePropFromContext,
otherPropFromContext,
someRegularPropNotFromContext,
}) => {
... // regular component logic
return(
... // regular component return
)
});
Have a function to select props from context (like redux map...)
function select(){
const { someValue, otherValue, setSomeValue } = useContext(AppContext);
return {
somePropFromContext: someValue,
setSomePropFromContext: setSomeValue,
otherPropFromContext: otherValue,
}
}
Write a connectToContext HOC
function connectToContext(WrappedComponent, select){
return function(props){
const selectors = select();
return <WrappedComponent {...selectors} {...props}/>
}
}
Put it all together
import connectToContext from ...
import AppContext from ...
const MyComponent = React.memo(...
...
)
function select(){
...
}
export default connectToContext(MyComponent, select)
Usage
<MyComponent someRegularPropNotFromContext={something} />
//inside MyComponent:
...
<button onClick={input => setSomeValueFromContext(input)}>...
...
Demo that I did on other StackOverflow question
Demo on codesandbox
The re-render avoided
MyComponent will re-render only if the specifics props from context updates with a new value, else it will stay there.
The code inside select will run every time any value from context updates, but it does nothing and is cheap.
Other solutions
I suggest check this out Preventing rerenders with React.memo and useContext hook.
I made a proof of concept on how to benefit from React.Context, but avoid re-rendering children that consume the context object. The solution makes use of React.useRef and CustomEvent. Whenever you change count or lang, only the component consuming the specific proprety gets updated.
Check it out below, or try the CodeSandbox
index.tsx
import * as React from 'react'
import {render} from 'react-dom'
import {CountProvider, useDispatch, useState} from './count-context'
function useConsume(prop: 'lang' | 'count') {
const contextState = useState()
const [state, setState] = React.useState(contextState[prop])
const listener = (e: CustomEvent) => {
if (e.detail && prop in e.detail) {
setState(e.detail[prop])
}
}
React.useEffect(() => {
document.addEventListener('update', listener)
return () => {
document.removeEventListener('update', listener)
}
}, [state])
return state
}
function CountDisplay() {
const count = useConsume('count')
console.log('CountDisplay()', count)
return (
<div>
{`The current count is ${count}`}
<br />
</div>
)
}
function LangDisplay() {
const lang = useConsume('lang')
console.log('LangDisplay()', lang)
return <div>{`The lang count is ${lang}`}</div>
}
function Counter() {
const dispatch = useDispatch()
return (
<button onClick={() => dispatch({type: 'increment'})}>
Increment count
</button>
)
}
function ChangeLang() {
const dispatch = useDispatch()
return <button onClick={() => dispatch({type: 'switch'})}>Switch</button>
}
function App() {
return (
<CountProvider>
<CountDisplay />
<LangDisplay />
<Counter />
<ChangeLang />
</CountProvider>
)
}
const rootElement = document.getElementById('root')
render(<App />, rootElement)
count-context.tsx
import * as React from 'react'
type Action = {type: 'increment'} | {type: 'decrement'} | {type: 'switch'}
type Dispatch = (action: Action) => void
type State = {count: number; lang: string}
type CountProviderProps = {children: React.ReactNode}
const CountStateContext = React.createContext<State | undefined>(undefined)
const CountDispatchContext = React.createContext<Dispatch | undefined>(
undefined,
)
function countReducer(state: State, action: Action) {
switch (action.type) {
case 'increment': {
return {...state, count: state.count + 1}
}
case 'switch': {
return {...state, lang: state.lang === 'en' ? 'ro' : 'en'}
}
default: {
throw new Error(`Unhandled action type: ${action.type}`)
}
}
}
function CountProvider({children}: CountProviderProps) {
const [state, dispatch] = React.useReducer(countReducer, {
count: 0,
lang: 'en',
})
const stateRef = React.useRef(state)
React.useEffect(() => {
const customEvent = new CustomEvent('update', {
detail: {count: state.count},
})
document.dispatchEvent(customEvent)
}, [state.count])
React.useEffect(() => {
const customEvent = new CustomEvent('update', {
detail: {lang: state.lang},
})
document.dispatchEvent(customEvent)
}, [state.lang])
return (
<CountStateContext.Provider value={stateRef.current}>
<CountDispatchContext.Provider value={dispatch}>
{children}
</CountDispatchContext.Provider>
</CountStateContext.Provider>
)
}
function useState() {
const context = React.useContext(CountStateContext)
if (context === undefined) {
throw new Error('useCount must be used within a CountProvider')
}
return context
}
function useDispatch() {
const context = React.useContext(CountDispatchContext)
if (context === undefined) {
throw new Error('useDispatch must be used within a AccountProvider')
}
return context
}
export {CountProvider, useState, useDispatch}
To my understanding, the context API is not meant to avoid re-render but is more like Redux. If you wish to avoid re-render, perhaps looks into PureComponent or lifecycle hook shouldComponentUpdate.
Here is a great link to improve performance, you can apply the same to the context API too