I am trying to implement a shared state into my application using the React context api.
I am creating an errorContext state at the root of my tree. The error context looks like so:
// ErrorContext.js
import React from 'react';
const ErrorContext = React.createContext({
isError: false,
setError: (error) => {}
});
export default ErrorContext;
Desired Result
I would like to update (consume) this context from anywhere in the app (specifically from within a promise)
Ideally the consume step should be extracted into a exported helper function
Example Usage of helper function
http.get('/blah')
.catch((error) => {
HelperLibrary.setError(true);
})
Following the react context docs:
I can create a provider like so :
class ProviderClass {
state = {
isError: false,
setError: (error) => {
this.state.isError = error;
}
}
render() {
return (
<ErrorContext.Provider value={this.state}>
{this.props.children}
</ErrorContext.Provider>
)
}
}
Then I can consume this provider by using the Consumer wrapper from inside a render call:
<ErrorContext.Consumer>
{(context) => {
context.setError(true);
}}
</ErrorContext.Consumer>
The Problem with this approach
This approach would require every developer on my team to write lots of boilerplate code every-time they wish to handle a web service error.
e.g. They would have to place ErrorContext.Consumer inside the components render() method and render it conditionally depending on the web service response.
What I have tried
Using ReactDOM.render from within a helper function.
const setError = (error) =>{
ReactDOM.render(
<ErrorContext.Consumer>
// boilerplate that i mentioned above
</ErrorContext.Consumer>,
document.getElementById('contextNodeInDOM')
) }
export default setError;
Why doesn't this work?
For some reason ReactDOM.render() always places this code outside the React component tree.
<App>
...
<ProviderClass>
...
<div id="contextNodeInDOM'></div> <-- even though my node is here
...
</ProviderClass>
</App>
<ErrorContext.Consumer></ErrorContext.Consumer> <-- ReactDOM.render puts the content here
Therefore there is no context parent found for the consumer, so it defaults to the default context (which has no state)
From the docs
If there is no Provider for this context above, the value argument
will be equal to the defaultValue that was passed to createContext().
If anyone can assist me on my next step, I am coming from Angular so apologies if my terminology is incorrect or if I am doing something extremely stupid.
You can export a HOC to wrap the error component before export, eliminating the boilerplate and ensuring that the context is provided only where needed, and without messing with the DOM:
// error_context.js(x)
export const withErrorContext = (Component) => {
return (props) => (
<ErrorContext.Consumer>
{context => <Component {...props} errorContext={context} />}
</ErrorContext.Consumer>
)
};
// some_component.js(x)
const SomeComponent = ({ errorContext, ...props }) => {
http.get('/blah')
.catch((error) => {
errorContext.setError(true);
})
return(
<div></div>
)
};
export default withErrorContext(SomeComponent);
Now that React 16.8 has landed you can also do this more cleanly with hooks:
const SomeComponent = props => {
const { setError } = useContext(ErrorContext)
http.get("/blah").catch(() => setError(true))
return <div />
}
Following the react context docs:
I can create a provider like so :
class ProviderClass {
state = {
isError: false,
setError: (error) => {
this.state.isError = error;
}
}
I don't think so - there should be setState used. There is a general rule in react "don't mutate state - use setState()" - abusing causes large part of react issues.
I have a feeling you don't understand context role/usage. This is more like a shortcut to global store eliminating the need of explicitly passing down props to childs through deep components structure (sometimes more than 10 levels).
App > CtxProvider > Router > Other > .. > CtxConsumer > ComponentConsumingCtxStorePropsNMethods
Accessing rendered DOM nodes with id is used in some special cases, generally should be avoided because following renders will destroy any changes made externally.
Use portals if you need to render sth somewhere outside of main react app html node.
Related
I have built an application that works well, but i now need to add some logic into each page that checks to see if the user has a subscription and if they do then send them to payment page to make a payment.
I have a hook (using SWR) that gets me the user session and within the hook it returns a boolean for isSubscribed. I intend to use this.
const session = useSession();
if(session.isLoading) {
return <></>;
}
if(!session.isSubscribed) {
/* redirect the user*/
}
return (
<p>HTML of the page / component</p>
)
An above example is what i currently do. But this solution requires me to copy pasta everytime to the page which obviously i can do, but it's no way efficient. I know that HOC exists and from what i know i an use a HOC to do this. But i have no idea how to write one that would fit this purpose.
As an added benefit, it would be useful to add the session as a prop to the 'new component' so that i dont have to call the hook twice.
Thanks for all and any help.
p.s. i mention it in the title, but i'm using NextJS. Not sure if this has any baring (i dont think it does, but worth mentioning)
You can create a wrapper HOC such as following;
const withSession = (Component: NextComponentType<NextPageContext, any, {}>) => {
const Session = (props: any) => {
const session = useSession();
if (session.isLoading) {
return <>Loading..</>
}
else {
return <Component {...props} />
}
};
// Copy getInitial props so it will run as well
if (Component.getInitialProps) {
Session.getInitialProps = Component.getInitialProps;
}
return Session;
};
And to use it in your page or component, you can simply do like;
const UserDetailPage: React.FC = (props) => {
// ...
// component's body
return (<> HI </>);
};
export default withSession(UserDetailPage);
I think this problem doesn't necessary require a HOC, but can be solved with a regular component composition. Depending on your actual use case, it may or may not be a simpler solution.
We could implement a Session component that would leverage the useSession hook and conditionally render components passed via the children prop:
const Session = props => {
const { isLoading } = useSession();
if (isLoading) {
return "Loading...";
}
return props.children;
};
Then nest the Page component into the Session:
const GuardedPage: React.FC<PageProps> = props => {
return (
<Session>
<Page {...props} />
</Session>
);
};
I see the question has already been answered, just wanted to suggest an alternative. One of the benefits of this approach is that we can wrap an arbitrary tree into the Session, and not just the Page.
Are you trying to return a page loading screen component and direct the user to the appropriate page based on thier subscription status? or isLoading handles one event and isSubscribed handles another?
Let's define (HOC) higher order component for the sake of your problem. By using HOC, logic can be modularized and redistributed throughout components. This HOC your creating should have the capability to call different methods on a single data source or one method to be applied across multiple components. For instance say you have an API component with 5 end points (login, subscribe, logout, unsubsubscribe) the HOC should have the ability to utilize any of the endpoints from any other component you use it in. HOC is used to create an abstraction that will allow you to define logic in a single place.
Your code calls one singular method to check if the session is in use of display the content of a page based on user subscription and page loading. Without seeing the components you are trying to use I can not determine the state that needs to be passed? but I will give it shot.
const session = useSession();
if(session.isLoading) {
return <></>;
}
if(!session.isSubscribed) {
/* redirect the user*/
}
return (
<p>HTML of the page / component</p>
)
First thing I see wrong in above code as a use case for an HOC component you have no export statement to share with other components. Also, why use 2 return statements for isLoading unless both conditions need to be checked (isLoading & isSubscribed) also, are these conditional statements depended on each other or seprate functions that can be called separately from another source? if you posted more of your code or the components you are pasting this into it would help?
To use this as an HOC in NEXT is essentially the same as react.
Dependant logic
const session = useSession(props);
// ad constructor and state logic
...
if(session.isLoading) {
return this.setState({isLoading: true});
} else {
return this.setState({isSubscribed: false});
}
Separate logic
const session = useSession(props);
// ad constructor and state logic
...
isLoading () => {
return this.setState({isLoading: true});
}
isSubscribed() => {
return this.setState({isSubscribed: true});
}
or something like this that uses routes...
import React, { Component } from 'react';
import { Redirect, Route } from 'react-router-dom';
export const HOC = {
isState: false,
isSubscribed(props) {
this.isState = false;
setTimeout(props, 100);
},
isLoading(props) {
this.isState = true;
setTimeout(props, 100);
}
};
export const AuthRoute = ({ component: Component, ...rest}) => {
return (
<Route {...rest} render={(props) => (
HOC.isAuthenticated === true ? <Component {...props} /> : <Redirect to='/' />
)}/>
)};
}
If you could share more of you code it would be more helpful? or one of the components you are having to copy and paste from your original HOC code. I would be easier than stabbing in the dark to assist in your problem but I hope this helps!
Cheers!
I'm trying to show loading while data is fetching from API and I want a higher-order component to achieve this. But my code causes an infinite loop
//home.tsx file
export class Home extends Component<Props, any> {
componentDidMount(): void {
this.props.getCharacters();
}
render() {
return (
<div></div>
)
}
}
const mapStateToProps = (state: AppState): StateProps => {
return {
loading:state.marvel.loading,
data: getResultsSelector(state),
};
};
const mapDispatchToProps: DispatchProps = {
getCharacters,
};
export default connect(mapStateToProps, mapDispatchToProps)(WithLoading(Home));
//hoc.tsx file
function WithLoading(Component:any) {
return function WihLoadingComponent({ loading, ...props }:any) {
console.log(loading)
if (!loading) return <Component {...props} />;
return <p>Hold on, fetching data might take some time.</p>;
};
}
export default WithLoading;
How can I fix this issue ?
I assume that getCharacters() is the function that fetches data from the API.
The Component is mounted depending on loading, but loading is set when the Component is mounted.
if (!loading) return <Component />; mounts the component.
componentDidMount invokes getCharacters()
getCharacters() sets loading = true
if (!loading) return <Component />; does not return the Component anymore.
when loading is done: loading = false
if (!loading) return <Component />; mounts the component again.
continue at 2.
The loading and mounting must be made independent of each other.
A. Either you should not invoke the loading inside the Component,
B. or you should not mount the Component conditionally.
A: separate the loading from the Component
As you probably want to create a generic HOC that shows some arbitrary Component only when loaded === true, you might prefer A., which means you have to design the Components so that they do not change loading themselves.
There might be ways to do that, e.g.:
componentDidMount(): void {
if( !this.props.dataHasAlreadyBeFetched ){
this.props.getCharacters();
}
}
but I think that would be bad style, and it seems to me that in your case it makes sense to separate it, because apparently the only reason your Component is mounted the first time is to invoke getCharacters, which unmounts the Component.
B: not un-mount the Component
The Component is always mounted (without if( !loading)), and itself has to be designed to render any content only if loading === true (and otherwise null). Of course, loading has to be passed to the Component as well.
This is not an issue but rather a question.
I wanted to use React solely for my Global state management and pass the todos through useReducer and useContext and I wonder if this is by any means a right way to go. I was called out by a react coder that this way the components rerender when they aren't supposed to but my element inspection shows only the changed component rerenders. Would please guide me as whether or not I can continue developing this way or have to revert back to Mobx or redux or many other third party state manager libraries.
Yes, you can and it's easier than ever thanks to the new hooks API! For very simple things like for instance, a global theme you can just create a context with React.createContext, and useContext.
For a more robust solution, you can actually implement a Flux architecture with a combination of useContext and useReducer. Here's one I made earlier.
// AcmeContext.js
import React, { useReducer, createContext } from 'react'
const AcmeContext = createContext({})
const actions = {
DO_SOMETHING: 'doSomething'
}
const actionCreators = dispatch => ({
updateComment: comment => {
dispatch({
type: actions.DO_SOMETHING,
payload: comment
})
}
})
// first paramter is your state, second is the action
let reducer = (currentState, { type, payload }) => {
switch (type) {
case actions.DO_SOMETHING:
// important: return a NEW new object for this context, don't change the old currentState
return { ...currentState, hello: payload }
default:
return
}
}
// this component wraps any of the child components that you want to share state with
function AcmeProvider({ children, initialState }) {
const [state, dispatch] = useReducer(reducer, initialState)
const actions = actionCreators(dispatch)
return (
<AcmeContext.Provider value={{ state, actions }}>
{children}
</AcmeContext.Provider>
);
}
export { AcmeContext, AcmeProvider }
Then, you wrap the component you want to provide the context to with the exported provider.
// App.jsx
import { AcmeProvider } from './AcmeContext'
import TestComponent from './TestComponent'
render((
<AcmeProvider initialState={{ hello: 'world' }}>
<TestComponent />
</AcmeProvider>
), document.querySelector('.app'))
Finally, you can call it from the child component.
// TestComponent.jsx
import { AcmeContext } from './AcmeContext'
export default () => {
const { state, actions } = useContext(AcmeContext)
return (
<div>
Hello {state.hello}!
<button onClick={() => actions.updateComment('me')}>Set response on onClick to 'me'</button>
</div>
)
}
This does have a couple of downsides to a full Redux implementation. You don't get the Redux dev tools and you don't get things like redux-thunk which means you'll have to add that logic to the component and get the component to update the context.
Yes you can totally use the default React APIs for full state management on a project. The introduction of hooks makes it easy to manage. useContext has slowly become my favourite hook because it removes the need for consumers and makes the JSX look a bit nicer.
If you are worried about things rerendering too many times, you can still use all of the tricks in the React Toolbox like React.memo.
I'm new to React and Next.js, so bear with me. But I've spent three hours searching, and I cannot for the life of me figure out what I'm doing wrong here.
this.props.test doesn't output anything, even though it seems like it should output 'test'.
It's like getInitialProps is never even called.
class Example extends React.Component {
static async getInitialProps() {
return {
test: 'test'
}
}
render() {
return (
<h1>Hi {this.props.test}</h1>
)
}
}
I came here with the same problem, and my Component was in /pages so the other answers didn't really help.
My issue was that I was using a custom App (/pages/_app.js) and it had a getInitialProps in it.
It's a little vague in the documentation, but in the custom app example it does mention (emphasis mine):
Only uncomment [getInitialProps] if you have blocking data requirements for every single page in your application. This disables the ability to perform automatic static optimization, causing every page in your app to be server-side rendered.
This essentially means that if your custom _app.js has a getInitialProps method, you are telling Next.js that this is the only blocking data and every other page component can just be assumed to not have or need their own getInitialProps. If you have one anyway, it'll be ignored and that's where my problem was.
This was solved by calling the getInitialProps on the page component from inside the getInitialProps for the custom App.
// pages/_app.js
const App = ({Component, pageProps }) => (
<React.Fragment>
<AppHeader />
<Component {...pageProps />
</React.Fragment>
)
App.getInitialProps = async ({Component, ctx}) => {
let pageProps = {}
if(Component.getInitialProps){
pageProps = await Component.getInitialProps(ctx)
}
return { pageProps }
}
// pages/home.js
const Home = ({name}) => <div>Hello world, meet {name}!</div>
Home.getInitialProps = async () => {
return { name: 'Charlie' }
}
export default Home
I'm not sure why you can't return pageProps directly (nothing gets passed), but it seems to be something special and it's working for me now, so..
Because getInitialProps only works for Pages in Next.js, the correct method for child components is componentDidMount with setState instead of props.
Just remove pages/_app.js file if it's your case.
That happens because _app.js overrides the defaults of NextJS.
If you still want to use getInitialProps along with _app.js you must declare this very function inside _app.js:
Inside _app.js:
export default class MyApp extends App {
static async getInitialProps(appContext){
// your logic goes here
return {}
}
render(){
const {Component} = this.props;
return (
<Layout>
<Component />
</Layout>
)
}
}
I'm using React-Native-Router-Flux for routing my app. The issue is that it seems like when a redux state changes, ALL the components under the Router gets rerendered, not just the "current" component.
So lets say I have 2 components under the Router: Register and Login and both share the same authenticationReducer. Whenever an authentication event (such as user registration or signin) fails, I want to display error Alerts.
The problem is that when an error is fired from one of the components, two Alerts show up at the same time, one from each component. I assumed when I am currently on the Register scene, only the error alert would show from the Register component.
However, it seems like both components rerender whenever the redux state changes, and I see 2 alerts (In the below example, both 'Error from REGISTER' and 'Error from SIGNIN').
Here are the components:
main.ios.js
export default class App extends Component {
render() {
return (
<Provider store={store}>
<Router>
<Scene key='root'>
<Scene key='register' component={Register} type='replace'>
<Scene key='signin' component={SignIn} type='replace'>
</Scene>
</Router>
</Provider>
);
}
}
Register.js
class Register extends Component {
render() {
const { loading, error } = this.props;
if (!loading && error) {
Alert.alert('Error from REGISTER');
}
return <View>...</View>;
}
}
const mapStateToProps = (state) => {
return {
loading: state.get("authenticationReducer").get("loading"),
error: state.get("authenticationReducer").get("error"),
};
};
export default connect(mapStateToProps)(Register);
SignIn.js
class SignIn extends Component {
render() {
const { loading, error } = this.props;
if (!loading && error) {
Alert.alert('Error from SIGNIN');
}
return <View>...</View>;
}
}
const mapStateToProps = (state) => {
return {
loading: state.get("authenticationReducer").get("loading"),
error: state.get("authenticationReducer").get("error"),
};
};
export default connect(mapStateToProps)(SignIn);
How do I change this so that only the REGISTER error message shows when I am currently on the Register Scene, and vice versa?
Thanks
Because of the way react-native-router-flux works, all previous pages are still "open" and mounted. I am not totally sure if this solution will work, because of this weird quirk.
Pretty strict (and easy) rule to follow with React: No side-effects in render. Right now you are actually doing a side-effect there, namely, the Alert.alert(). Render can be called once, twice, whatever many times before actually rendering. This will, now, cause the alert to come up multiple times as well!
Try putting it in a componentDidUpdate, and compare it to the previous props to make sure it only happens once:
componentDidUpdate(prevProps) {
if (this.props.error && this.props.error !== prevProps.error) {
// Your alert code
}
}
I am not totally convinced that this will actually work, as the component will still update because it is kept in memory by react-native-router-flux, but it will at least have less quirks.
I solved this by creating an ErrorContainer to watch for errors and connected it to a component that uses react-native-simple-modal to render a single error modal throughout the app.
This approach is nice because you only need error logic and components defined once. The react-native-simple-modal component is awesomely simple to use too. I have an errors store that's an array that I can push errors to from anywhere. In the containers mapStateToProps I just grab the first error in the array (FIFO), so multiple error modals just "stack up", as you close one another will open if present.
container:
const mapStateToProps = (
state ) => {
return {
error: state.errors.length > 0 ? state.errors[0] : false
};
};
reducer:
export default function errors (state = [], action) {
switch (action.type) {
case actionTypes.ERRORS.PUSH:
return state.concat({
type: action.errorType,
message: action.message,
});
case actionTypes.ERRORS.POP:
return state.slice(1);
case actionTypes.ERRORS.FLUSH:
return [];
default:
return state;
}
}