NextJS - Passing client-side props from app.js to specific components - javascript

In traditional React, a common pattern is to define the Routers at the entry point, and pass whatever props you need to whichever component needs them, since they're all defined
Eg,
<Switch>
<Route exact path="/">
<Home prop1={prop1}/>
</Route>
<Route path="/about">
<About prop1={prop1} prop2={prop2}/>
</Route>
<Route path="/dashboard">
<Dashboard />
</Route>
</Switch>
It's not clear to me how to do this in NextJS. The entry point _app.js has a generic that's used for all components. What would be the best way to pass prop1 to Home and About, but not Dashboard?
To be clear, these are client-side props, not server-side or static props

You can pass page specific props in getServerSideProps like below
import { GetServerSideProps } from "next";
const PageA = () => {
}
export const getServerSideProps: GetServerSideProps = async (ctx) => {
return {
props: {
forbidden: true
}
}
}
export default PageA;
Then you can control that prop value in _app.js file and take action
const App = ({ Component, pageProps }) => {
if (pageProps.forbidden) {
return <Page403 />;
}
return (
<Provider store={store}>
<Component {...pageProps} />
</Provider>
)
}
So, think reversely.
UPDATE
Okay, so you want _app.js to be your starting point. Here's a way to do so.
_app.js
const App = ({ Component, pageProps }) => {
if (pageProps.forbidden) {
return <Page403 />;
}
return (
<Provider store={store}>
{pageProps.forbidden ? <Component {...pageProps} /> : <Component {...pageProps} testProp={true} />}
</Provider>
)
}
In this technic, we still need to mark the pages we want that specific prop to be existed. For instance, we want that prop to be existed in pages which are not forbidden. Page A, in this case, should not get that prop.
import { GetServerSideProps } from "next";
const PageA = (props) => {
console.log('PageA props', props);//we should not see testProp here
}
export const getServerSideProps: GetServerSideProps = async (ctx) => {
return {
props: {
forbidden: true
}
}
}
export default PageA;
But Page B should get it.
import { GetServerSideProps } from "next";
const PageB = (props) => {
console.log('PageB props', props);//we should see testProp here
}
export const getServerSideProps: GetServerSideProps = async (ctx) => {
return {
props: {}
}
}
export default PageB;
You can modify the logic according to your needs.

You can use getStaticProps, see the code below:
export async function getStaticProps(context) {
return {
props: {}, // will be passed to the page component as props
// export this function from each page you want to pass props, in your
// case have this function on About, Home and Dashboard pages.
}
}
for more check this link: getStaticProps

Related

React react-router-dom private route not working when auth token is asynchronously loaded from cookies

I have a react/redux app, using 'react-router-dom' for routing. I am using the following in my App.js (tags are react-bootstrap, in order to provide private routes. Expected behaviour is to be redirected to /login if, and only if, the user is not logged in (based on the presence of a cookie). Actual behaviour is that the app immediately redirects to /login, even when the user is logged in.
class MainContainer extends Component {
constructor(props) {
super(props);
this.props.loadAuthCookie();
}
PrivateRoute = ({ component: ChildComponent, ...rest }) => {
return (
<Route
{...rest}
render={(props) => {
if (!this.props.auth.loggedIn && !this.props.auth.authPending) {
return <Redirect to="/login" />;
} else {
return <ChildComponent {...props} />;
}
}}
/>
);
};
render() {
const { PrivateRoute } = this;
return (
<Router>
<Container fluid id="root">
<Header />
<Switch>
<Row className="pageContainer">
<Col>
<PrivateRoute exact path="/" component={HomeScreen} />
<Route
exact
path="/clusters/"
component={clusterOverviewScreen}
/>
<Route exact path="/login" component={LoginPage} />
</Col>
</Row>
</Switch>
</Container>
</Router>
);
}
}
const mapStateToProps = (state) => {
return {
auth: state.auth,
};
};
const mapDispatchToProps = (dispatch) => {
return {
loadAuthCookie: () => {
return dispatch(loadAuthCookie());
},
};
};
const RootContainer = connect(
mapStateToProps,
mapDispatchToProps
)(MainContainer);
export default class App extends Component {
render() {
return (
<Provider store={store}>
<RootContainer />
</Provider>
);
}
}
The function this.props.loadAuthCookie dispatches a redux action which pulls the cookie that contains the auth token (if present), and puts it into the state.
Initial state is
{
"authPending": false
"loggedIn": false
"authToken": null
}
Which becomes:
{
"authPending": true
"loggedIn": false
"authToken": null
}
and finally:
{
"authPending": false
"loggedIn": true
"authToken": TOKEN
}
I am relatively new to react. My guess is that by the time the PrivateRoute function runs, the redux action has not yet set the state, and therefore, the PrivateRoute component redirects to the login page. This guess is based on logging props.auth.loggedIn in the private route - this returns false, but by the time the login page to which I am redirected has finished loading, the component props state that this.props.loggedIn is true.I am not sure how to fix this, though.
Edit: loadAuthCookie action:
export const loadAuthCookie = () => (dispatch) => {
dispatch(AuthReducer.setAuthPending());
try {
const cookie = Cookies.get("token");
if (cookie) {
dispatch(AuthReducer.setLoginSuccess(cookie));
}
} catch (err) {
console.log(err);
dispatch(AuthReducer.setLogout());
}
};
The PrivateRoute looks like it's implemented incorrectly.
It should be designed to 'wait' until either authPending becomes false or a loggedIn becomes true.
But in reality it requires both of them to be true for it to NOT redirect to login. So implement to 'wait' if authPending: true until loggedIn:true.
Here is table of the outcomes.
Also don't use conditions like !loggedIn && !authPending where readability is terrible especially when you've been working hours on it. I've over looked this because I can't be asked to comprehend what I'm seeing.
<Route
{...rest}
render={(props) => {
if (this.props.auth.loggedIn) {
return <ChildComponent {...props} />;
} else if (this.props.auth.authPending) {
return <LoadingScreen/> // Or could simply div with text. Not different component
// Then state updates after x seconds and re-renders
} else {
return <Redirect to="/login" />;
}
}}
/>
Edit: I actually got the logic wrong, either authPending & loggedIn can be true to redirect to the child element because of the ! inverse operator.
function test(first, second) {
if (!first && !second) {
console.log('login');
} else {
console.log('child');
}
}
test(true, true);
test(false,false);
test(true, false);
test(false, true);
output:
child
login
child
child
See still can't get my head around!;
I think Tony is right; you probably need to call this.props.loadAuthCookie(); later in the lifecycle of this component.
The constructor is run when (and only when) the component is instantiated; as opposed to when it's mounted or rendered. The constructor is usually called once, and before mounting or rendering.
For this reason, calling this.props.loadAuthCookie() might work inside of componentDidMount as well, but I'd go with Tony's suggestion and try it in the render() function.

How to protect routes, not to be accessed directly through URL?

//this route is define in app.js
<Route path="/edit" component={Edit}/>
//this is navlink
<NavLink to={{pathname:"/edit",state:{index:index}}}>Edit</NavLink>
// class
class Edit extends Component {
constructor(props){
super(props)
this.state = {
index:props.location.state.index,
title:'',
};
}
componentDidMount(){
axios.get("http://localhost:5000/gettitle",{headers: {token: Cookies.get('adtoken')}})
.then(response => {
this.setState({
title:response.data.jobs[this.state.index].title,
})
})
.catch(error => {
console.log(error)
})
}
render() {
}
}
export default Edit;
When I click on this Navlink it moves to /edit with props, but when I directly write /edit through URL it gives errors because it is accessing /edit component without props
How can I protect /edit so that it cant be accessed directly through URL?
Thanks
You can use PrivateRoute component:
const PrivateRoute = ({ component: Component, ...rest }) => (
<Route
{...rest}
render={props =>
props?.location.state?.index ? (
<Component {...props} />
) : (
<Redirect to="/404" />
)
}
/>
);
Here an example
If you are using react-router-v3 or less you can use Route onEnter callback
<Route onEnter={checkIfValidUrl}></Route>
and in callback function you will get get replace function. which you can use to redirect to desire page.
const checkIfValidUrl = (nextState, replace, cb) => {
// Your condition goes here.
}
But if you are using react-router-v4 or higher can create a High order component (HOC) to check if that route is valid route or not.
If your application supports server side rendering you can write code on server to handle this scenario.
It does seem like you are using React Router, thus my answer will be for that library.
In order to protect /edit you could trigger <Redirect /> if props are missing.
EditPage component would look something like this:
import React from 'react'
import { Redirect } from 'react-router-dom'
const EditPage = ({ index }) => {
if(index){
return <Redirect to="/" />
} else {
return <div>my protected page</div>
}
}
export default EditPage

How to setup Reactjs with Google Analytics?

I am trying to get GA to track all my pages that are being changed by React Router v4.
I seen this code using the library: react-ga
history.listen(location => {
// ReactGA.set({ page: location.pathname })
// ReactGA.pageview(location.pathname)
})
but I don't think this will log the first page.
I seen this but I don't know how to set it up on my site
https://github.com/react-ga/react-ga/wiki/React-Router-v4-withTracker
My index.js looks like this
ReactDOM.render(
<Provider routingStore={routingStore} domainStores={DomainStores} uiStores={uiStores}>
<Router history={history}>
<ErrorBoundary FallbackComponent={ErrorFallbackComponent}>
<StripeProvider apiKey={stripeKey}>
<AppContainer />
</StripeProvider>
</ErrorBoundary>
</Router>
</Provider>
,
document.getElementById('app')
);
then inside "appConainter" I have "Switch" with my routes in it
<Switch>
<Route
exact
path="n"
component={}
/>
</Switch>
You need to include it in your AppContainer component:
import withTracker from './withTracker' which is a file that you manually create.
Then make the file called withTracker.jsx and put these contents in there:
import React, { Component, } from "react";
import GoogleAnalytics from "react-ga";
GoogleAnalytics.initialize("UA-0000000-0");
const withTracker = (WrappedComponent, options = {}) => {
const trackPage = page => {
GoogleAnalytics.set({
page,
...options,
});
GoogleAnalytics.pageview(page);
};
// eslint-disable-next-line
const HOC = class extends Component {
componentDidMount() {
// eslint-disable-next-line
const page = this.props.location.pathname + this.props.location.search;
trackPage(page);
}
componentDidUpdate(prevProps) {
const currentPage =
prevProps.location.pathname + prevProps.location.search;
const nextPage =
this.props.location.pathname + this.props.location.search;
if (currentPage !== nextPage) {
trackPage(nextPage);
}
}
render() {
return <WrappedComponent {...this.props} />;
}
};
return HOC;
};
export default withTracker;
Make sure you have react-ga installed.
Then everywhere you have a route defined in AppContainer you need to call it:
<Route component={withTracker(App, { /* additional attributes */ } )} />
The documentation you linked shows how to do it.

How can I make Saga watch events before the React components are mounted?

If I set the browser url to: /uni-classi-virtuali/gestisci/1
The function componentWillMount of the component DettaglioClasseVirtuale (where I have the fetch resource call) is called before the rootSaga started.
Is there a standard way to make saga fetch a resource on application startup?
This my saga watcher for fetching an item of resource "classivirtualiuni":
export function* watchGetClasseVirtualiUni() {
yield takeEvery(GET_CLASSE.REQUEST, getClasseVirtualiUni)
}
export function* getClasseVirtualiUni(action) {
try {
const data = yield call(fetchResource, `classivirtualiuni/${action.id}`)
if (data.success) {
yield put(getClasse.success(data.content))
} else {
yield put(getClasse.failure(data.message))
}
} catch (error) {
yield put(getClasse.failure(error.message))
}
}
called by a simple rootsaga:
export default function* rootSaga() {
yield [
// ...
// ...
watchGetClasseVirtualiUni()
]
}
This is my App class handled by react-router:
class App extends Component {
render() {
return (
<Router history={browserHistory}>
<Route path={'/uni-classi-virtuali'} component={Main} >
<IndexRedirect to={'/uni-classi-virtuali/gestisci'} />
<Route path={'crea'} component={CreateClasseVirtualeUni} />
<Route path={'gestisci'} component={ClasseVirtualeList} >
<Route path={':id'} component={DettaglioClasseVirtuale} />
</Route>
<Route path={'iscriviti'} component={IscrivitiClasseVirtuale} />
</Route>
</Router>
)
}
}
This is the component calling the fetch data:
class DettaglioClasseVirtuale extends Component {
componentWillMount() {
// dispatch the GET_CLASSE.REQUEST action
this.fetchClasseVirtuale(this.props.classeId)
}
// ...
}
This is the core of the application:
function configureStore(initialState) {
const sagaMiddleware = createSagaMiddleware()
let finalCreateStore = applyMiddleware(thunk, sagaMiddleware)(createStore)
const store = finalCreateStore(reducer, initialState);
store.runSaga = sagaMiddleware.run
store.close = () => store.dispatch(END)
return store;
}
const store = configureStore()
store.runSaga(rootSaga)
ReactDOM.render(
<div>
<Provider store={store}>
<App />
</Provider>
</div>,
document.getElementById('virtual-class')
)
I found a clean and simple workaround: putting a ready action in the rootSaga and waiting for it in the Main component.
export default function* rootSaga() {
yield [
// ...
// ...
watchGetClasseVirtualiUni(),
put(ready())
]
}
class App extends Component {
render() {
return (
<Loader isLoaded={this.props.ready}>
<Router history={browserHistory}>
<Route path={'/uni-classi-virtuali'} component={Main} >
{//...}
</Route>
</Router>
</Loader>
)
}
}
I had a similar problem using react-boilerplate. I fixed it by using compose().
Don't do this:
import { useInjectSaga } from 'utils/injectSaga';
export function App() {
useInjectSaga({ key: 'app', saga }); //it's already too late, we're already loading the component
return <div />;
}
export default compose()(App);
Do this:
import injectSaga from 'utils/injectSaga';
const key = 'app';
export function App() {
return <div />;
}
const withSaga = injectSaga({ key, saga }); //this comes into play before the component loads
export default compose(withSaga)(App);
Source
You can add getClasseVirtualiUni to your rootSaga which runs on app launch rather than listening to actions through watchGetClasseVirtualiUni. The only issue that remains is to figure out how to pass classeId which is passed through the component.

Access redux store from redux-simple-router children

I'm trying to figure out how to access the redux store from within route so I can dispatch actions from within the route.
Here's what my top level Component looks like:
class App extends Component {
render() {
return (
<div>
{ children }
</div>
);
}
}
My redux-simple-router code looks like:
render(
<Provider store={store}>
<Router history={history}>
<Route path="/" component={App}>
<IndexRoute component={ Home } />
<Route path="/example" component={ ExampleRoute } />
</Route>
</Router>
</Provider>,
rootElement
)
If I dump props from within the ExampleRoute component, I don't have access to the store. Any help appreciated!
You should use connect from react-redux to get dispatch and current state from the store. It is outlined in the redux docs here: http://rackt.org/redux/docs/basics/UsageWithReact.html
Here is your Example component:
//...
import { connect } from 'react-redux'
//...
export class Example extends Component {
render () {
const { dispatch, thingName } = this.props
return (
<button onClick={ () => {
dispatch(myAwesomeActionCreator())
}}>{ thingName }</button>
);
}
}
export default connect(state => state)(Example)
Some good examples of how to use connect can be found in the react-redux docs: https://github.com/rackt/react-redux/blob/master/docs/api.md#examples
I was able to get this working with "Monkeypatch" middleware, but there's got to be a better way.
First I created a function to monkeypatch the children variable. This function takes the child, the dispatch and the store as arguments, and returns an updated children variable with keys for the store and dispatch:
function routeStoreMiddleware (children, dispatch, store) {
return {
...children,
props: {
...children.props,
dispatch: dispatch,
store: store
}
}
}
Then I simply updated the component that already has access to the dispatch and store to consume the middleware function:
class App extends Component {
render() {
return (
<div>
{ routeStoreMiddleware(children, dispatch, store) }
</div>
);
}
}
Since the poorly named routeStoreMiddleware function simply returns an updated children object, it still works.
Now I can dispatch events and display data from within the ExampleRoute component.
import React, { Component } from 'react';
import { myAwesomeActionCreator } from '../actions.js'
export class Example extends Component {
render () {
const { dispatch, store } = this.props
return (
<button onClick={ () => {
dispatch(myAwesomeActionCreator())
}}>{ store.thingName }</button>
);
}
}
Yay!
Please note:
I've been reading a lot here about how to make middleware properly in redux, but I haven't had time yet to understand it fully. There's a better way than I've done here.

Categories

Resources