I am using React native and I have a context variable post, it has an attribute called name and I have defined a function called onChange to set it.
import React, { useState } from "react";
const PostContext = React.createContext({
content: "",
onChange: (newPostContent: string) => {},
});
export const PostContextProvider = ({ children }) => {
const [content, setContent] = useState("");
const postChangeHandler = (newPostContent: string) => {
setContent(newPostContent);
};
return (
<PostContext.Provider
value={{ content, onChange: postChangeHandler }}
>
{children}
</PostContext.Provider>
);
};
export default PostContext;
Now I have a page on which I want to fetch a post from Amplify's GraphQL API and set its content to my context variable, so I can use it on other pages.
import React, { useEffect, useContext } from "react";
import { API, graphqlOperations} from "aws-amplify";
import PostContext from "./context/post-context";
const post = useContext(PostContext);
const fetchPost = async () => {
const {data: {getPost: { postContent },},} = await API.graphql(
graphqlOperation(`
query GetPost {
getPost(id: "${some post Id}") {
content
}
}
`)
);
post.onChange(postContent)
}
useEffect(()=>{
fetchPost()
}, [])
useEffect(()=>{
console.log(post.content)
}, [post])
What I expect is that in the async function, the execution is blocked until postContent (because of the await and then it's value is assigned to the context variable, or its update is schedualed (that's why I have also included a useEffect to console.log the value of post.content. But it is not updated and its value remains an empty screen. Can somebody help me with this? I am learning React native how this work, so a detailed answer that lets me know what I am doing wrong is appreciated.
I am using a function to call an api and i have added navigation on 401 and i want to use navigation here. But as the hooks can be called from the component only. So anybody can tell me how can i add navigation here. So someone i can import here and use while passing in the function.
const GetApiRequestWithToken = async (url, params, headers) => {
return new Promise((resolve, reject) => {
axios.get(base_url_address + url, { headers: headers }).then(resp => {
if (resp.status == 401) {
UnAuthorizedLogout()
} else {
resolve(resp)
}
}).catch((error) => {
resolve(error.response)
});
})
}
So this is a function and how can i import navigation in this. from hooks or some other way.
As this is not allowing to import here
import { useNavigation } from '#react-navigation/native';
and i don't want that everytime, i call GetApiRequestWithToken then pass navigation to it.
You can use a navigation provider pattern to achieve this. You can create a utility file like the following:
// util/navigation.js
import React from 'react';
export const navigationRef = React.createRef();
export const navigate = (routeName, params) => {
navigationRef.current?.navigate(routeName, params);
};
This stores the navigator reference in a local variable. You can see that it requires the navigator object to be passed in by an external component. I would recommend calling this function in your top-level navigation stack component. You most likely already have a component that looks something like the below:
// NavigationContainer.js
import React from 'react';
import { createStackNavigator } from '#react-navigation/stack';
import { NavigationContainer } from '#react-navigation/native';
import { navigationRef } from 'util/navigation'; // from util
import ExampleStack from './ExampleStack';
const Stack = createStackNavigator();
export default () => {
return (
<NavigationContainer ref={navigationRef}> {/* store ref */}
<Stack.Navigator
initialRouteName="Example"
...
>
<Stack.Screen
name="Example"
component={ExampleStack}
/>
</Stack.Navigator>
</NavigationContainer>
);
};
You can see here that the component created by createNavigationContainer will pass the ref to your navigation provider util above.
Finally, you can use the NavigationContainer created in this second file inside your App.js.
// App.js
import NavigationContainer from './NavigationContainer';
export default () => (
<NavigationContainer />
);
Any other functions of the navigator can be added to navigationUtil.js now, for example goBack. You can now use the utility in your axios request like so:
// axios util
import { navigate } from './navigationUtil.js'; // new
const GetApiRequestWithToken = async (url, params, headers) => {
return new Promise((resolve, reject) => {
axios.get(base_url_address + url, { headers: headers }).then(resp => {
if (resp.status == 401) {
navigate('UnauthorizedLogoutScreen'); // new
UnAuthorizedLogout();
} else {
resolve(resp)
}
}).catch((error) => {
resolve(error.response)
});
})
}
I hope this is clear, feel free to ask if something has not been covered.
I think this can be done using a custom hook.
import React, { useEffect } from 'react';
import { useNavigation } from '#react-navigation/native';
import axios from 'axios';
export default function useFetchAPI(url, params, headers) {
const [result, setResult] = React.useState(null);
const navigation = useNavigation();
useEffect(() => {
axios
.get(base_url_address + url, { headers: headers })
.then((resp) => {
console.log('resp: ', resp);
if (resp.status == 401) {
// this will navigate to your UnAuthorizedLogout page
navigation.navigate('UnAuthorizedLogout');
} else {
// otherwise, set the response to result state
setResult(resp);
}
})
.catch((error) => {
setResult(error.response);
});
}, [url])
return result;
}
What I need to do is to render the react native routes based on the users auth status. Right now I am doing this the wrong way, by having an interval running to check for auth status change:
import React, { useState, useEffect } from 'react';
import { AppLoading } from 'expo';
import { checkAuth } from './auth';
import { LoggedInRoutes, LoggedOutRoutes } from './router';
export default () => {
const [isReady, setReady] = useState(false);
const [loggedIn, setLoggedIn] = useState(false);
useEffect(() => {
setInterval(() => {
checkAuth()
.then((res) => { setLoggedIn(res); setReady(true); console.log('checked..') })
.catch((err) => alert(err));
}, 1500);
}, [loggedIn]);
if (!isReady) {
return (
<AppLoading
onFinish={() => setReady(true)}
/>
);
}
return (
loggedIn ? <LoggedInRoutes /> : <LoggedOutRoutes />
);
}
But obviously that is quite bad. I am using async storage to save the user when he authenticates and remove him from storage when he clicks the logout button.
Is there a way to check for changes in async storage and re-render the routes? or run a function that changes loggedIn state when user click login/logout button?
I would recommend to use switchNavigator in react navigation
reactnavigation.org/docs/4.x/auth-flow – mr-nobody 40 secs ago Edit Delete
this approach will works like a charm.
import React, {useState, useEffect} from 'react';
import OnBoardingRoutes from './onBoarding.routes';
import AppRoutes from './app.routes';
import checkFirstUsage from "./checkFirstUsage/path";
const Routes: React.FC = () => {
const [loading, setLoading] = useState(true)
const [firstUsage,setFirstUsage] =useState(null);
useEffect(() => {
async function check() {
const fU = await checkFirstUsage()
setFirstUsage(fU)
setLoading(false)
}
check()
},[])
if (loading) return null // or any better component
return firstUsage ? <OnBoardingRoutes /> : <AppRoutes />;
};
export default Routes;
I have a couple of buttons that acts as routes. Everytime the route is changed, I want to make sure the button that is active changes.
Is there a way to listen to route changes in react router v4?
I use withRouter to get the location prop. When the component is updated because of a new route, I check if the value changed:
#withRouter
class App extends React.Component {
static propTypes = {
location: React.PropTypes.object.isRequired
}
// ...
componentDidUpdate(prevProps) {
if (this.props.location !== prevProps.location) {
this.onRouteChanged();
}
}
onRouteChanged() {
console.log("ROUTE CHANGED");
}
// ...
render(){
return <Switch>
<Route path="/" exact component={HomePage} />
<Route path="/checkout" component={CheckoutPage} />
<Route path="/success" component={SuccessPage} />
// ...
<Route component={NotFound} />
</Switch>
}
}
To expand on the above, you will need to get at the history object. If you are using BrowserRouter, you can import withRouter and wrap your component with a higher-order component (HoC) in order to have access via props to the history object's properties and functions.
import { withRouter } from 'react-router-dom';
const myComponent = ({ history }) => {
history.listen((location, action) => {
// location is an object like window.location
console.log(action, location.pathname, location.state)
});
return <div>...</div>;
};
export default withRouter(myComponent);
The only thing to be aware of is that withRouter and most other ways to access the history seem to pollute the props as they de-structure the object into it.
As others have said, this has been superseded by the hooks exposed by react router and it has a memory leak. If you are registering listeners in a functional component you should be doing so via useEffect and unregistering them in the return of that function.
v5.1 introduces the useful hook useLocation
https://reacttraining.com/blog/react-router-v5-1/#uselocation
import { Switch, useLocation } from 'react-router-dom'
function usePageViews() {
let location = useLocation()
useEffect(
() => {
ga.send(['pageview', location.pathname])
},
[location]
)
}
function App() {
usePageViews()
return <Switch>{/* your routes here */}</Switch>
}
You should to use history v4 lib.
Example from there
history.listen((location, action) => {
console.log(`The current URL is ${location.pathname}${location.search}${location.hash}`)
console.log(`The last navigation action was ${action}`)
})
withRouter, history.listen, and useEffect (React Hooks) works quite nicely together:
import React, { useEffect } from 'react'
import { withRouter } from 'react-router-dom'
const Component = ({ history }) => {
useEffect(() => history.listen(() => {
// do something on route change
// for my example, close a drawer
}), [])
//...
}
export default withRouter(Component)
The listener callback will fire any time a route is changed, and the return for history.listen is a shutdown handler that plays nicely with useEffect.
import React, { useEffect } from 'react';
import { useLocation } from 'react-router';
function MyApp() {
const location = useLocation();
useEffect(() => {
console.log('route has been changed');
...your code
},[location.pathname]);
}
with hooks
With hooks:
import { useEffect } from 'react'
import { withRouter } from 'react-router-dom'
import { history as historyShape } from 'react-router-prop-types'
const DebugHistory = ({ history }) => {
useEffect(() => {
console.log('> Router', history.action, history.location)
}, [history.location.key])
return null
}
DebugHistory.propTypes = { history: historyShape }
export default withRouter(DebugHistory)
Import and render as <DebugHistory> component
import { useHistory } from 'react-router-dom';
const Scroll = () => {
const history = useHistory();
useEffect(() => {
window.scrollTo(0, 0);
}, [history.location.pathname]);
return null;
}
With react Hooks, I am using useEffect
import React from 'react'
const history = useHistory()
const queryString = require('query-string')
const parsed = queryString.parse(location.search)
const [search, setSearch] = useState(parsed.search ? parsed.search : '')
useEffect(() => {
const parsedSearch = parsed.search ? parsed.search : ''
if (parsedSearch !== search) {
// do some action! The route Changed!
}
}, [location.search])
in this example, Im scrolling up when the route change:
import React from 'react'
import { useLocation } from 'react-router-dom'
const ScrollToTop = () => {
const location = useLocation()
React.useEffect(() => {
window.scrollTo(0, 0)
}, [location.key])
return null
}
export default ScrollToTop
In some cases you might use render attribute instead of component, in this way:
class App extends React.Component {
constructor (props) {
super(props);
}
onRouteChange (pageId) {
console.log(pageId);
}
render () {
return <Switch>
<Route path="/" exact render={(props) => {
this.onRouteChange('home');
return <HomePage {...props} />;
}} />
<Route path="/checkout" exact render={(props) => {
this.onRouteChange('checkout');
return <CheckoutPage {...props} />;
}} />
</Switch>
}
}
Notice that if you change state in onRouteChange method, this could cause 'Maximum update depth exceeded' error.
For functional components try useEffect with props.location.
import React, {useEffect} from 'react';
const SampleComponent = (props) => {
useEffect(() => {
console.log(props.location);
}, [props.location]);
}
export default SampleComponent;
For React Router v6 & React Hooks,
You need to use useLocation instead of useHistory as it is deprecated
import { useLocation } from 'react-router-dom'
import { useEffect } from 'react'
export default function Component() {
const history = useLocation();
useEffect(() => {
console.log('> Router', history.pathname)
}, [history.pathname]);
}
With the useEffect hook it's possible to detect route changes without adding a listener.
import React, { useEffect } from 'react';
import { Switch, Route, withRouter } from 'react-router-dom';
import Main from './Main';
import Blog from './Blog';
const App = ({history}) => {
useEffect( () => {
// When route changes, history.location.pathname changes as well
// And the code will execute after this line
}, [history.location.pathname]);
return (<Switch>
<Route exact path = '/' component = {Main}/>
<Route exact path = '/blog' component = {Blog}/>
</Switch>);
}
export default withRouter(App);
I just dealt with this problem, so I'll add my solution as a supplement on other answers given.
The problem here is that useEffect doesn't really work as you would want it to, since the call only gets triggered after the first render so there is an unwanted delay.
If you use some state manager like redux, chances are that you will get a flicker on the screen because of lingering state in the store.
What you really want is to use useLayoutEffect since this gets triggered immediately.
So I wrote a small utility function that I put in the same directory as my router:
export const callApis = (fn, path) => {
useLayoutEffect(() => {
fn();
}, [path]);
};
Which I call from within the component HOC like this:
callApis(() => getTopicById({topicId}), path);
path is the prop that gets passed in the match object when using withRouter.
I'm not really in favour of listening / unlistening manually on history.
That's just imo.
I'm trying to get Koa to isomorphically render a html string it receives from react-router.
Here's the code I've been trying to make work:
server.js
import koa from 'koa';
import React from "react";
import Router from "react-router";
import routes from "./routes";
const server = koa();
const port = process.env.NODE_ENV || 8000;
server.use(function *() {
try {
yield Router.run(routes, this.request.url, function (Handler) {
var content = React.renderToString(<Handler/>)
this.body = content
})
}
catch (err) {
this.status = err.status || 500;
this.body = err.message;
this.app.emit('error', err, this);
}
})
server.listen(port, function(err) {
if (err) throw err;
console.log("Server running on: http://localhost:"+port)
})
routes.js
import React from "react"
import {Route, DefaultRoute} from "react-router"
import Main from "./components/main"
export default (
<Route path="/">
<DefaultRoute handler={Main} name="main" />
</Route>
)
main.js
import React from "react"
const Main = React.createFactory(React.createClass ({
render () {
return (
<div>HELLO</div>
)
}
}))
export default Main
Getting several errors:
Warning: Component(...): No render method found on the returned
component instance: you may have forgotten to define render in your
component or you may have accidentally tried to render an element
whose type is a function that isn't a React component.
Warning: Don't set the props property of the React element. Instead,
specify the correct value when initially creating the element.
TypeError: Can't add property context, object is not extensible
Warning: Something is calling a React component directly. Use a
factory or JSX instead. See: https://fb.me/react-legacyfactory
for those who needs an answer with this, hope this will help you guys.
server.js
import koa from 'koa';
import React from "react";
import Router from "react-router";
import routes from "./routes";
const server = koa();
let handler;
server.use(function *(next) {
Router.run(routes, this.request.url, (Handler) => {
handler = React.renderToString(<Handler/>);
});
yield next;
});
server.use(function *() {
this.body = handler;
});
export default server;
components/index.js
import React from "react";
const Main = React.createClass ({
render () {
return (
<div>HELLO</div>
);
}
});
export default Main;
i dont see the need to create factory for this one.
I was getting the "calling a React component directly" warning too and fixed it by yielding a promise
function *() {
this.body = yield new Promise(resolve => {
Router.run(routes, this.req.url, Handler => {
resolve(React.renderToString(<Handler/>));
});
});
}