Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Day5 - React Router & fake Login #3

Merged
merged 4 commits into from Nov 4, 2018
Merged
Changes from all commits
Commits
File filter...
Filter file types
Jump to…
Jump to file or symbol
Failed to load files and symbols.

Always

Just for now

@@ -96,10 +96,45 @@ one way or another redux is an implementation to observer design pattern (aka pu
# Day 4
1- add dashboard actions to load initial data (user profile)
2- create progress bar gauge
3- create metrics container with static data
4- try css grid to make metrics boxes responsive
5- add request builder to centralize request exit point
6- add user repo (not working yet)
Day 4 included working on many components to finish the read part of the dashboard and components interactions
see this PR for all the changes with comments explaining.
https://github.com/blabadi/react-nutracker/pull/2
blog:http://dev.basharallabadi.com/2018/10/part-4-nutracker-reactjs-application.html
# Day 5
React Router
In this day I want to:
- add react router and profile page
Problem: Reading router params in component (ex: if i want the current date to show in url `/dashboard/:date` to bookmark it)
- currently the period (date filter) is stored in the state and changed by actions (when user moves forward/backward)
- react router suggest not to synchronize state with routes see readings [1]
- to go around it they suggest to pass router params as filter and read them in `mapStateToProps(state, props => { match: { params }})`
- components rendered by `<Route component={}>` get match in props by defualt
- others need to use this: `withRouter(connect(mapState,mapDispatch)(LComp))`. this is also needed if our component doesn't get rerendered on route changes. (faced this
issue in the Nav component).
- This means that we have two sources of truth router & redux store, which can become messy to wrap our head around in big applications since we won't quickly know if the component gets everything from redux store or it could get stuff from router in its own properties.
- the above also means that anything changing the route (changes filters) shouldn't be an action but a `<Link />` instead.
for now I want to keep this simple so I'll use the router without params (no specific url per date)
NavLink in router already provider accessiable links with active class property to style
readings:
<ol>
<li>https://reacttraining.com/react-router/web/guides/redux-integration</li>
<li>https://redux.js.org/advanced/usagewithreactrouter</li>
</ol>
# Day 6
PropTypes & Unit testing
# Things to check later:
- https://redux.js.org/recipes/serverrendering
- https://redux.js.org/recipes/implementingundohistory
- reach hooks !
2,486 README.md

Large diffs are not rendered by default.

Large diffs are not rendered by default.

@@ -8,6 +8,7 @@
"react": "^16.4.2",
"react-dom": "^16.4.2",
"react-redux": "^5.0.7",
"react-router-dom": "^4.3.1",
"react-scripts": "1.1.5",
"redux": "^4.0.0",
"redux-thunk": "^2.3.0"
@@ -24,7 +25,7 @@
"proxy": {
"/api": {
"target": "http://18.222.254.118:8080",
"changeOrigin" : true
"changeOrigin": true
}
}
}
@@ -1,27 +1,35 @@
import React, { Component } from 'react'
import logo from './logo.svg'
import './App.css'
import Dashboard from './containers/Dashboard'

import { Provider } from 'react-redux'
import {createStore, applyMiddleware} from 'redux'
import thunkMiddleware from 'redux-thunk'
import rootReducer from './reducers'

import {
BrowserRouter as Router,
} from 'react-router-dom'
import logo from './logo.svg'
import './App.css'
import Nav from './containers/Nav';
import {routerOutput} from './routes';
const store = createStore(rootReducer, applyMiddleware(thunkMiddleware));

class App extends Component {
render() {
console.log('in render app');
return (
<Provider store={store}>
<div className="App container">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h1 className="App-title">Welcome to NuTracker</h1>
</header>
<div className="content">
<Dashboard></Dashboard>
<Router>

This comment has been minimized.

Copy link
@blabadi

blabadi Nov 4, 2018

Author Owner

to use react router we wrap the root component with so that sub components can access router properties (current location & params etc)

<div className="App container">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h1 className="App-title">Welcome to NuTracker</h1>
</header>
<Nav/>
<div className="content">
{routerOutput()}

This comment has been minimized.

Copy link
@blabadi

blabadi Nov 4, 2018

Author Owner

this will render the proper content based on the url see routes.js

</div>
</div>
</div>
</Router>
</Provider>
);
}
@@ -21,9 +21,6 @@ class Dashboard extends Component {
return (
<div className="row">
<div className="col-md-12">
<div className="mb-4">
Hello, {this.props.user.name}
</div>
<div className="mb-4">
<FoodSearch></FoodSearch>
</div>
@@ -3,7 +3,6 @@ import Entry from '../presentational/entry/Entry';
import {connect} from 'react-redux';
import React, {Component} from 'react'
import Util from '../Util';

class DayEntries extends Component {
componentDidMount() {
this.props.fetchEntries(Util.dateToDatestamp(this.props.from), Util.dateToDatestamp(this.props.to));
@@ -42,7 +41,7 @@ const mapDispatchToProps = dispatch => ({
fetchEntries: (s, e) => dispatch(fetchEntries(s, e))
})

const mapStateToProps = (state) => {
const mapStateToProps = (state, props) => {

This comment has been minimized.

Copy link
@blabadi

blabadi Nov 4, 2018

Author Owner

In case we want to pass custom properties for the component that are not managed by redux, for example the router properties we can use this 2nd argument that redux passes to us to be able to do that, see: https://redux.js.org/advanced/usagewithreactrouter#reading-from-the-url

This comment has been minimized.

Copy link
@blabadi

blabadi Nov 4, 2018

Author Owner

I didn't do that here to keep things simple now

return {
entries: state.entries,
from: state.period.from,
@@ -53,4 +52,4 @@ const mapStateToProps = (state) => {
export default connect(
mapStateToProps,
mapDispatchToProps
)(DayEntries)
)(DayEntries);
@@ -0,0 +1,34 @@
import React from 'react'
import {
Redirect
} from 'react-router-dom'
import {AuthStore} from '../repos/UserRepo'


export class Login extends React.Component {

This comment has been minimized.

Copy link
@blabadi

blabadi Nov 4, 2018

Author Owner

this is a simple fake login component.
1- user clicks a button
2- it invokes AuthStore.authenticate, on the call back it redirects to the caller (this prop is set in routes.js

state = {
redirectToReferrer: false
};

login = () => {
AuthStore.authenticate(() => {
this.setState({ redirectToReferrer: true });
});
};

render() {
const { from } = this.props.location.state || { from: { pathname: "/" } };
const { redirectToReferrer } = this.state;

if (redirectToReferrer) {
return <Redirect to={from} />;
}

return (
<div>
<p>You must log in to view the page at {from.pathname}</p>
<button onClick={this.login}>Log in</button>
</div>
);
}
}
@@ -0,0 +1,41 @@
import React, {Component} from 'react'
import { AuthStore } from '../repos/UserRepo';
import {NavLink, withRouter} from 'react-router-dom'
import { connect } from 'react-redux';

//TODO make responsivity better
class Nav extends Component {

This comment has been minimized.

Copy link
@blabadi

blabadi Nov 4, 2018

Author Owner

a simple navigation bar that allows the user to switch between profile/dashboard

render() {
if (AuthStore.isAuthenticated) {
return (
<nav className="navbar navbar-expand-lg navbar-light bg-light" >
<NavLink className="navbar-brand d-none d-lg-inline" to="/">NuTracker</NavLink>

This comment has been minimized.

Copy link
@blabadi

blabadi Nov 4, 2018

Author Owner

NavLink is component provided by react router it automatically adds the css class active to the current location

<ul className="navbar-nav">
<li className="nav-item">
<NavLink className="nav-link" activeClassName="active" to="/dashboard">Dashboard</NavLink>
</li>
<li className="nav-item">
<NavLink className="nav-link" to="/profile">Profile</NavLink>
</li>
</ul>
<ul className="navbar-nav ml-auto">
<li className="nav-item">
<div className="user-name-container">
<span>{this.props.username}</span>
</div>
</li>
</ul>
</nav>)
}
return null;
}
}

const mapStateToProps = (state) => ({
username : state.user.name
});

//had to add with router here because this component will not be rerendered since it's not
//a router component and no props change happen currently since user is loaded in dashboard
//if we remove this the routing will work but the navigation links won't reflect the current active link
export default withRouter(connect(mapStateToProps, {})(Nav));

This comment has been minimized.

Copy link
@blabadi

blabadi Nov 4, 2018

Author Owner

read this comment to see some cases where you have to use withRouter

This comment has been minimized.

Copy link
@blabadi

blabadi Nov 4, 2018

Author Owner

withRouter will force the component to update when the router state changes, and this component will be able to access the router location (using this.props.location)

@@ -0,0 +1,9 @@
import React, {Component} from 'react'

export default class Profile extends Component {

This comment has been minimized.

Copy link
@blabadi

blabadi Nov 4, 2018

Author Owner

empty profile page (has its own route)

render() {
return (<div>
Welcome to profile.
</div>)
}
}
@@ -17,7 +17,7 @@ export default class DateNavigator extends Component {
render() {
return (
<div className="date-nav-container">
<div class="controls-container">
<div className="controls-container">
<button type="button" className="btn btn-link float-left date-control-btn-back" onClick={() => this.onDateChange(-1)}>
<span className="date-control fa fa-arrow-circle-o-left">&lt;</span>
</button>
@@ -12,7 +12,6 @@

.search-result-list{
position: relative;
z-index: 9999;
background-color:white;
border: 1px solid #c8c8c8;
border-bottom-left-radius: 10px;
@@ -21,6 +20,14 @@
overflow-y: auto;
}

@media(min-width: 800px) {
.search-result-list{
position: absolute;
width: 50%;
z-index: 9999;
}
}

.search-result:hover {
color: #f8f8f8;
background-color: #337ab7;
@@ -1,4 +1,17 @@
import requestBuilder from "./RequestBuilder";

export const AuthStore = {

This comment has been minimized.

Copy link
@blabadi

blabadi Nov 4, 2018

Author Owner

this just simulates login/logout operations, in the future this will probably use localStorage or cookie to store an authentication token that will be read by RequestBuilder.js

isAuthenticated: false,
authenticate(cb) {
this.isAuthenticated = true;
setTimeout(cb, 100); // fake async
},
signout(cb) {
this.isAuthenticated = false;
setTimeout(cb, 100);
}
};

class UserRepo {
findUser(id) {
return requestBuilder.execute({
@@ -0,0 +1,39 @@
import React from 'react'
import Dashboard from './containers/Dashboard'
import {Login} from './containers/Login'
import Profile from './containers/Profile'
import { AuthStore } from './repos/UserRepo'
import {
Route,
Switch,
Redirect
} from 'react-router-dom'


const renderIfAuthenticated = (Component) => props => AuthStore.isAuthenticated ?

This comment has been minimized.

Copy link
@blabadi

blabadi Nov 4, 2018

Author Owner

this method is used by <PrivateRoute> below to check the authentication status, and either allow or redirect to the login page , we pass the current location to tell the login component to redirect back to it if login is successful.

(<Component {...props} />) :
(<Redirect
to={{
pathname: "/login",
state: { from: props.location }
}}
/>
)

const PrivateRoute = ({ component: Component, ...rest }) => (

This comment has been minimized.

Copy link
@blabadi
<Route
{...rest}
render={renderIfAuthenticated(Component)}
/>
);


export const routerOutput = () =>
(
<Switch>

This comment has been minimized.

Copy link
@blabadi

blabadi Nov 4, 2018

Author Owner

Switch will only render the first matching route (from top to bottom).

because without the switch router by default will render all matching routes.
if the first route didn't have exact it will be matched with every url and you will get stuck in a redirect loop.

one addition can be is a catch-all route to match any incorrect urls (at the end of all routes) :
<Route component={NotFoundComponent} />

<Redirect from="/" exact to="/dashboard" />
<PrivateRoute path="/profile" component={Profile}></PrivateRoute>
<PrivateRoute path="/dashboard" component={Dashboard}></PrivateRoute>
<Route path="/login" component={Login}></Route>
</Switch>
);
ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.