I've spent about 1 week reading up on redux before plunging into anything sizeable. After completing most of the tutorials I've done I realised, ok I understand redux but how the hell do I make a complex system :P
I started going about by creating my systems actions:
function requestLogin(creds) {
return {
type: LOGIN_REQUEST,
isFetching: true,
isAuthenticated: false,
creds
}
}
function receiveLogin(user) {
return {
type: LOGIN_SUCCESS,
isFetching: false,
isAuthenticated: true,
id_token: user.id_token
}
}
function loginError(message) {
return {
type: LOGIN_FAILURE,
isFetching: false,
isAuthenticated: false,
message
}
}
But how can I with each router used (using react-router) check to see if the user has a session after storing the users logged in state in the redux state?
I wanted to create something that gets executed with each view. Just simply write a function that exec()'s in each view?
Yes, you create a function that executes whenever you go to a route that requires a login.
import LoginActions from '../loginActions';
const requireLogin = (nextState, replace) => {
store.dispatch(LoginActions.CheckLogin());
const {login} = store.getState();
if (!login.isAuthenticated)
replace('/login');
};
Call it in your router:
<Router component={Root}>
<Route path="/dashboard" onEnter={requireLogin} component={dashboard}/>
</Router>
You can implement auth filter for paths requiring the user to be authenticated using Higher Order Components.
Create wrapping component
import React from 'react';
import { connect } from 'react-redux';
export default function(ComposedComponent) {
class AuthFilter extends React.Component {
// triggered when component is about to added
componentWillMount() {
if (!this.props.userAuthenticated) {
console.log("navigate to login");
this.context.router.push('/login');
}
}
// before component updated
componentWillUpdate(nextProps) {
if (!nextProps.userAuthenticated) {
console.log("navigate to login");
this.context.router.push('/login');
}
}
render() {
return <ComposedComponent {...this.props} />
}
}
AuthFilter.contextTypes = {
router: React.PropTypes.func.isRequired
}
function mapStateToProps(state) {
return { userAuthenticated: state.authenticated };
}
return connect(mapStateToProps)(AuthFilter);
}
And then add wrapping to your route component as:
Route path="/asset" component={AuthFilter(AssetRoute)}/>
Related
I'm new to React and Firebase and I'm trying get the user object that's populated after sign in/sign up to a new component so I can access it and create a database for each user, however I can't seem to access the this.state.user from another component. Here's my code:
import React, { Component } from 'react';
import fire from './config/Fire';
import Login from './Login';
import Home from './Home';
class App extends Component {
constructor(props){
super(props);
this.state = {
user: {},
}
}
// After this component renders, we will call on auth lister which will begin auth listener process
componentDidMount(){
this.authListener();
}
authListener() {
fire.auth().onAuthStateChanged((user) => {
console.log(user);
if (user) {
this.setState({ user });
} else {
this.setState({ user: null });
}
});
}
render() {
return (
<div className="App">
{this.state.user ? (<Home user={this.state.user}/>) : (<Login/>)}
</div>
);
}
}
export default App;
and in my new component i have:
componentDidMount(){
fire.auth().onAuthStateChanged((user) => {
if (this.props.user){
console.log(this.props.user);
// const userId = user.uid;
// fire.database().ref(`users/${userId}`).set({
// username: '',
// age: ''
// });
} else {
console.log('No user');
}
});
}
You are listening for the user in componentDidMount of the App component, then you are passing that data into the home component in <Home user={this.state.user}/>
This means that you can refrence the user object inside the Home component as this.props.user.
You don't need to call the onAuthStateChanged handler again inside Home, you already did that in the App Component.
Hope that helps.
I encountered this problem when I was testing my newly created action and reducer. The prop is not being updated even though I'm setting it to a fixed value within my reducer.
Component:
class <ComponentName> extends Component {
componentDidMount() {
login()
}
render() {
if(this.props.isLogged)
return (
<App/>
);
else
return (
<ErrorScreen/>
);
}
}
function mapStateToProps(state) {
return {
isLogged:state.auth.isLogged
}
}
const mapDispatchToProps = (dispatch) => {
return {
login: () => dispatch(login())
};
};
export default connect(mapStateToProps,mapDispatchToProps)(<ComponentName>)
Action:
export function login() {
return {
type:"TEST"
}
}
Reducer:
const initState = {
isLogged: false,
}
export default (state=initState, action) => {
switch(action.type) {
case "TEST":
return {
...state,
isLogged: true
}
break;
default:
return state
}
}
Combine Reducer:
import {combineReducers} from 'redux'
import AuthenticationReducer from './authenticationReducer'
export default combineReducers({
auth: AuthenticationReducer
})
Provider:
import React, {Component} from "react";
import <ComponentName> from './app/screens/<ComponentName>'
import store from './app/store'
import {Provider} from 'react-redux'
export default () =>
<Provider store={store}>
<<ComponentName>/>
</Provider>;
Been trying to debug this for some time now. I still don't know why this is happening. Maybe I implemented it wrongly? If there are some files I forgot to include, please inform me. Thanks and have a nice day!
The reason your code isn't working as expected is because you're calling the login() action creator, rather than the login() method that is returned from mapDispatchToProps() (and injected into the props of <ComponentName/>).
Try revising your code by adding this.props before your call to login() like so:
class <ComponentName> extends Component {
componentDidMount() {
// Update this line here so that the login() method
// injected by connect() is called (ie via this.props)
this.props.login()
}
render() {
if(this.props.isLogged)
return <App/>
else
return <ErrorScreen/>
}
}
I want to test in all components whether the user has connection to the internet.
I could use NetInfo in each component, but since I am using redux, I thought it could be done easier with a middleware(?).
I have used
import { createStore, applyMiddleware } from 'redux';
const netInfo = store => next => action => {
const listener = (isConnected) => {
store.dispatch({
type: types.NET_INFO_CHANGED,
isConnected,
});
};
NetInfo.isConnected.addEventListener('change', listener);
NetInfo.isConnected.fetch().then(listener);
return next(action);
};
const store = createStore(AppReducer, applyMiddleware(netInfo));
where AppReducer is just combineReducers(navReducer, netInfoReducer, ...).
It does seem to work, but I am really worried if this performs well enough. It seems it is only run once, but I am never removing the listener or anything.
Is this how you normally would do if you want to populate all components with an isConnected variable?
I would create a Higher-Order Component for this:
import React, { Component } from 'react';
import { NetInfo } from 'react-native';
function withNetInfo(WrappedComponent) {
return class extends Component {
constructor(props) {
super(props);
this.state = {};
this.handleChange = this.handleChange.bind(this);
NetInfo.isConnected.fetch().then(this.handleChange);
}
componentDidMount() {
NetInfo.isConnected.addEventListener('change', this.handleChange);
}
componentWillUnmount() {
NetInfo.isConnected. removeEventListener('change', this.handleChange);
}
handleChange(isConnected) {
this.setState({ isConnected });
}
render() {
return <WrappedComponent isConnected={this.state.isConnected} {...this.props} />;
}
}
}
export default withNetInfo;
Then you can wrap whatever component you would like to render:
class MyComponent extends Component {
render() {
const { isConnected } = this.props;
return(
<View>
<Text>
{`Am I connected? ${isConnected}`}
</Text>
</View>
);
}
}
export default withNetInfo(MyComponent);
Bonus: if you want to keep the statics methods of your original component (if you have defined some) you should use the package hoist-non-react-statics to copy the non-react specific statics:
import React, { Component } from 'react';
import { NetInfo } from 'react-native';
import hoistStatics from 'hoist-non-react-statics';
function withNetInfo(WrappedComponent) {
class ExtendedComponent extends Component {
constructor(props) {
super(props);
this.state = {};
this.handleChange = this.handleChange.bind(this);
NetInfo.isConnected.fetch().then(this.handleChange)
}
componentDidMount() {
NetInfo.isConnected.addEventListener('change', this.handleChange);
}
componentWillUnmount() {
NetInfo.isConnected. removeEventListener('change', this.handleChange);
}
handleChange(isConnected) {
this.setState({ isConnected });
}
render() {
return <WrappedComponent isConnected={this.state.isConnected} {...this.props} />;
}
}
return hoistStatics(ExtendedComponent, WrappedComponent);
}
export default withNetInfo;
There shouldn't be a performance issue using middleware to keep "isConnected" in your redux store, but you would want to make sure the listener is only added once. I use https://github.com/michaelcontento/redux-middleware-oneshot to achieve that.
I considered middleware, too, but was also afraid how to handle the sub/unsub. I've decided to go with adding and removing the listener in componentDidMount and componentWillUnmount of my AppContainer class, which holds the rest of the app in my MainNavigator. This class' lifecycle should follow that of the app, and thus make sure to sub/unsub correctly. I am, however, also going to use a redux action to set the status and listen to it in the relevant views to show a 'no connection' banner.
I am experiencing an issue with Redux (used along with React). Here is my code:
/// <reference path="../../typings/index.d.ts"/>
import "./polyfill.ts";
import * as React from 'react'
import * as ReactDOM from 'react-dom'
import { createStore, combineReducers } from 'redux'
import { Provider, dispatch } from 'react-redux'
import { Router, Route, browserHistory, IndexRedirect } from 'react-router'
import { syncHistoryWithStore, routerReducer } from 'react-router-redux'
import { Form, Input } from './components/form.tsx'
import app from './reducers.ts'
const store = createStore(
combineReducers({
app,
routing: routerReducer
}),
{},
window.devToolsExtension && window.devToolsExtension()
);
import App from './views/app.tsx';
import Reminders from './views/reminders.tsx';
import Calendar from './views/calendar.tsx';
import Settings from './views/settings.tsx';
import Groups from './views/groups.tsx';
import Courses from './views/courses.tsx';
import Homework from './views/homework.tsx';
// Create an enhanced history that syncs navigation events with the store
const history = syncHistoryWithStore(browserHistory, store);
class Application extends React.Component<any,any> {
constructor(props){
super(props);
}
render() {
var socket = io();
socket.on('connect', function () {
setTimeout(function(){
store.dispatch({
type: "SOCKET_ESTABLISHED",
socket: socket
});
}, 1000);
});
socket.on('DATA', function(data){
console.log("DATA");
console.log(data);
})
console.log(Groups);
return <Provider store={store}>
<Router history={history}>
<Route path="/" component={App}>
<IndexRedirect to="/reminders" />
<Route path="reminders" component={Reminders}/>
<Route path="calendar" component={Calendar}/>
<Route path="settings" component={Settings}>
<Route path="groups" component={Groups}/>
</Route>
<Route path="homework" component={Homework} />
</Route>
</Router>
</Provider>
}
}
ReactDOM.render(
<Application/>,
document.getElementById('mount')
);
My reducer file looks like the following:
export default function app(state={
connection: {
socket: null,
online: false
}
}, action: any) {
console.log(action);
switch(action.type){
case "SOCKET_ESTABLISHED":
console.log(state.connection);
return Object.assign(state, {
connection: {
socket: action.socket,
online: true
}
});
default:
return state;
}
};
And my App component is the following:
import * as React from 'react';
import { connect } from 'react-redux';
import Top from '../components/top.tsx';
import Nav from '../components/nav.tsx';
class AppClass extends React.Component<any,any> {
constructor(props){
super(props);
}
render() : JSX.Element {
return <div>
<Top/>
<Nav/>
{this.props.connection.online ? this.props.children : "Offline." }
</div>
}
}
var App = connect(function(state){
return {
connection: state.app.connection
};
})(AppClass);
export default App;
So let me be clear, I am using the connect function to sync store and App props. Upon page load, a socket connection is created and the action SOCKET_ESTABLISHED which changes store.connection.online to true. Everything works fine, the Redux dev tools show that the store is correctly updated and online is now true. If I look with the React dev tools the Connect(AppClass) component, its storeState is up to date, but its child component, which is AppClass, does not have the right props, this.props.app.connection.online is false where it should be true. I have seen that it could come from a state mutation, but using Object.assign a new store is returned (The function is the MDN Polyfill).
Hope I was clear and thank you !
(Note that I am using Typescript for compiling although I might have been neglecting some interface definitions)
You're modifying the state object inside of the reducer. You need to copy state to a new object, and then modify the new one.
Update this:
return Object.assign(state, {
connection: {
socket: action.socket,
online: true
}
});
To this:
return Object.assign({}, state, {
connection: {
socket: action.socket,
online: true
}
});
// or this
return {
...state,
connection: {
socket: action.socket,
online: true
}
}
Redux receives the new state from the reducer and compares it to the previous state. If you modify the state directly, the new state and previous state are the same object. It's similar to doing this.
var state = { foo: 'bar' };
state.test = 'test';
var newState = state;
// state === newState # true
Instead, you want to copy the object before modifying
var state = { foo: 'bar' };
var newState = { ...state, test: 'test' };
// state === newState # false
mapStateToProps function must return new state object to triger rerender of the component.
Try changing
return state
to
return {...state}
Creating a web page for our ski lodge, we are renting it out to people. I have a Google spreadsheet with pricing, dates and wether it is available for rent that specific date.
My previous page was written in AngularJS with TabletopJS fetching the data from the spreadsheet, but I thought it would be a good experience writing it in React and Redux without TabletopJS.
How would I go about fetching data from the JSON that the Google spreadsheet spits out? I have looked at the Redux tutorial of async actions, but I find it hard to implement with my own code.
Do you guys have any tips on how I should do this?
EDIT: Here is my angularjs code:
tabletop-config.js
'use strict';
angular
.module('myApp')
.config(tableTopConfig);
function tableTopConfig (TabletopProvider) {
TabletopProvider.setTabletopOptions({
key: 'https://docs.google.com/spreadsheets/d/1GJHfKQy24BFKNkYVjCfFtar1OCd4vJ8_TvFRheMKH90/pubhtml',
simpleSheet: true
});
}
bookingController.js
(function () {
'use strict';
angular
.module('myApp')
.controller('bookingController', bookingController);
function bookingController($scope, Tabletop) {
var vm = this;
Tabletop.then(function(ttdata) {
vm.data = ttdata[0];
});
}
})();
You'll need* redux-thunk or something similar to handle asynchronous actions. The fetchTable function below has to be updated with whatever your API is to get the spreadsheet data.
(*NOTE: Technically you can do without redux-thunk, but it will only be more complicated.)
// actions/spreadsheet.js
// --- Action-creators ---
function requestSpreadsheet() {
return {
type: 'SPREADSHEET_REQUEST'
}
}
function receiveSpreadsheet(data) {
return {
type: 'SPREADSHEET_RECEIVED',
payload: {
data: data
}
}
}
function receiveSpreadsheetError(error) {
return {
type: 'SPREADSHEET_FAIL',
payload: {
error: error
}
}
}
// --- API ---
function fetchTable(tableInfo) {
// Code related to API here. Should just return a promise.
// Someting like...
return fetch('spreadsheet url here')
}
// --- Thunks ---
function getSpreadsheetData(tableInfo) {
return function (dispatch, getState) {
// Tell reducers that you are about to make a request.
dispatch(requestSpreadsheet())
// Make the request, then tell reducers about
// whether it succeeded or not.
return fetchTable(tableInfo).then(
data => dispatch(receiveSpreadsheet(data)),
error => dispatch(receiveSpreadsheetError(error))
)
}
}
Then you'll need to have a reducer or reducers that listen for those actions and update the application state accordingly. You should think about how you want the application state to be shaped. I'm assuming nothing else about your application.
// reducers/spreadsheet.js
const initialState = {
spreadsheetData: {}
loading: false,
errorMsg: ''
}
function spreadsheet(state = {}, action = {}) {
switch (action.type) {
case 'SPREADSHEET_REQUEST':
return {
...state,
loading: true
}
case 'SPREADSHEET_RECEIVED':
return {
...state,
loading: false,
spreadsheetData: action.payload.data,
errorMsg: ''
}
case 'SPREADSHEET_FAIL':
return {
loading: false,
errorMsg: action.payload.error
}
}
}
Then you have your view logic in React that treats the entire view as a function taking in the app state and returning the HTML you would like to display. You need to npm install --save react-redux to allow your React components to "listen" for changes in your redux app state, so they will re-render accordingly.
// containers/Spreadsheet.js
import { connect } from 'react-redux'
import SpreadsheetComponent from '../components/Spreadsheet'
// This function tells your SpreadsheetComponent about the
// parts of the app state that it should "listen" to. The
// component will receive them as normal react props.
// These fields can be named whatever is most convenient.
function mapStateToProps(appState) {
return {
spreadsheetData: state.spreadsheetData,
loading: state.loading,
errorMsg: state.errorMsg
}
}
//
export default connect(mapStateToProps)(SpreadsheetComponent)
// components/Spreadsheet.js
import React from 'react';
function Spreadsheet(props) {
return (
<h1>Here is the spreadsheet!</h1>
<ul>
{props.spreadsheetData.map(row => {
return (
<li>
<RowStuff row={row} />
</li>
)
})}
</ul>
)
}
And how do you wire all this stuff together?
// configureStore.js
import React from 'react'
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux'
import { render } from 'react-dom'
import thunk from 'redux-thunk';
import App from './components/App'
import rootReducer from './reducers/index';
const store = createStore(
rootReducer,
applyMiddleware(thunk)
);
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)