Currently within my app I have multiple Routes using React Router that are wrapped within the Redux provider.
Provider
<Provider store={store}>
<Router>
<Route exact path ="/" component = {LoginPage}></Route>
<Route exact path="/login" component ={LoginPage}/>
<Route path ="/change" component={MasterPage}/>
<Route path="/change/form" component={ChangeForm}/>
<Route path="/change/table" component={ExpandTable} />
</Router>
</Provider>
As it stands I'm confused how I'm supposed to being passing/accessing the state of the store within the components.
The only state that I really need to be storing is the current status of the user login and want to use this to change what is rendered on the other routes.
Reducer
I have a reducer which looks like:
export default (state = {}, action) => {
switch (action.type) {
case 'SIMPLE_ACTION':
return {
loggedIn: action.loggedIn,
User: action.User,
Group: action.Group
}
default:
return state
}
}
Action
And an action which looks like
export const simpleAction = () => dispatch => {
dispatch({
type: 'SIMPLE_ACTION',
payload: data
})
}
Now I'm currently working under the assumption that connecting them to the store using
export default connect(mapStateToProps, mapDispatchToProps)(LoginForm);
would allow me to access the store within that component, but it seems that isn't the case.
I'm a little confused as to how to access the store state within the component and also how to properly write the action/reducer to change this state.
So I found where I was going wrong, I needed to pass the props to the route using:
render={(props) => <LoginPage {...props} login={store.getState()} />}>
And in my configureStore() I was passing rootReducer instead of the simple reducer I had made.
Your assumption is correct. It's a good practice to follow the connect pattern. You can create containers for components and do the connection there.
Something like this:
LoginPage.js
const LoginPage = ({loggedIn}) => {
console.log(loggedIn);
return null; // Write your markup here
}
LoginPage.propTypes = {
loggedIn: PropTypes.bool
}
export default LoginPage;
LoginPageContainer.js
import LoginPage from './LoginPage';
let LoginPageContainer = ({loggedIn}) => {
return <LoginPage loggedIn={loggedIn} />
};
LoginPageContainer.propTypes = {
loggedIn: PropTypes.bool
//...
}
const mapStateToProps = state => {
return {
loggedIn: state.........
};
};
LoginPageContainer = connect(mapStateToProps)(LoginPageContainer);
export default LoginPageContainer;
NOTE: This approach is usually used for more complex logic. In your case the LoginPage itself can be a container, so you don't need separate component.
Related
I'm learning redux.
Wrote some simple components for fetching data from API.
Everything worked fine until I added routing.
I'm wrapping my Router with Provider. I have route '/cars' and component VisibleCarList which accesses store using connect. When accessing '/cars' via Link or direct URL(tried both, same result) mapStateToProps is not invoked at all, can't see any redux props.
However if I go to for example '/example' with routed App component which has VisibleCarList in it everything works fine.
Spent a few hours already and I still don't understand why it can't connect to the store. Any ideas?
index.js
const store = createStore( reducer, composeEnhancers(
applyMiddleware(thunkMiddlewares)))
ReactDOM.render(
<Provider store={store}>
<Router>
<Route path="/example" component={App}/>
<Route path="/cars" component={VisibleCarList}/>
</Router>
</Provider>
, document.getElementById('root'));
VisibleCarList
export class VisibleCarList extends Component {
componentDidMount() {
this.props.getCarsPage(0);
}
render() {
...
}
}
const mapStateToProps = (state) => {
return{
cars: state.cars,
pagination: state.pagination,
}
}
const mapDispatchToProps = {
getCarsPage: fetchCarPage,
setPage: setCurrentPage,
}
export default connect(mapStateToProps, mapDispatchToProps)(VisibleCarList)
App.js
function App(props) {
return (
<Box bgcolor="primary.light">
<NavBar />
<VisibleCarList></VisibleCarList> //WORKS FINE
</Box>
);
}
export default App;
I think the import statement may be wrong.
You have to use import VisibleCarList from '...'; rather than import { VisibleCarList } from '...';.
I'm experimenting with Hooks without any state management tool (such as Redux), to get the same kind of behavior/structure I could have by using a traditional structure of classes + redux.
Usually, with a class base code I would:
ComponentDidMount dispatch to Call the API
Use actions and reducers to store the data in Redux
Share the data to any component I want by using mapStateToProps
And here where the problem is by using Hooks without Redux: 'Share the DATA with any component'.
The following example is the way I have found to share states between components by Hooks:
//app.js
import React, { useReducer } from 'react'
import { BrowserRouter } from 'react-router-dom'
import Routes from '../../routes'
import Header from '../Shared/Header'
import Footer from '../Shared/Footer'
export const AppContext = React.createContext();
// Set up Initial State
const initialState = {
userType: '',
};
function reducer(state, action) {
switch (action.type) {
case 'USER_PROFILE_TYPE':
return {
userType: action.data === 'Student' ? true : false
};
default:
return initialState;
}
}
const App = () => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<BrowserRouter>
<AppContext.Provider value={{ state, dispatch }}>
<Header userType={state.userType} />
<Routes />
<Footer />
</AppContext.Provider>
</BrowserRouter>
)
}
export default App
// profile.js
import React, { useEffect, useState, useContext} from 'react'
import { URLS } from '../../../constants'
import ProfileDeleteButton from './ProfileDeleteButton'
import DialogDelete from './DialogDelete'
import api from '../../../helpers/API';
// Import Context
import { AppContext } from '../../Core'
const Profile = props => {
// Share userType State
const {state, dispatch} = useContext(AppContext);
const userType = type => {
dispatch({ type: 'USER_PROFILE_TYPE', data: type }); <--- Here the action to call the reducer in the App.js file
};
// Profile API call
const [ profileData, setProfileData ] = useState({});
useEffect(() => {
fetchUserProfile()
}, [])
const fetchUserProfile = async () => {
try {
const data = await api
.get(URLS.PROFILE);
const userAttributes = data.data.data.attributes;
userType(userAttributes.type) <--- here I am passing the api response
}
catch ({ response }) {
console.log('THIS IS THE RESPONSE ==> ', response.data.errors);
}
}
etc.... not important what's happening after this...
now, the only way for me to see the value of userType is to pass it as a prop to the <Header /> component.
// app.js
<BrowserRouter>
<AppContext.Provider value={{ state, dispatch }}>
<Header userType={state.userType} /> <--passing here the userType as prop
<Routes />
<Footer />
</AppContext.Provider>
</BrowserRouter>
Let's say that I want to pass that userType value to children of <Routes />.
Here an example:
<AppContext.Provider value={{ state, dispatch }}>
<Routes userType={state.userType} />
</AppContext.Provider>
and then, inside <Routes /> ...
const Routes = () =>
<Switch>
<PrivateRoute exact path="/courses" component={Courses} userType={state.userType} />
</Switch>
I don't like it. It's not clean, sustainable or scalable.
Any suggestions on how to make the codebase better?
Many thanks
Joe
You don't need to pass the state as a prop to every component. Using context you can gain access to state properity in your reducer inside every child component of parent Provider. Like you have already done in the Profile.js
const {state, dispatch} = useContext(AppContext);
State property here contains state property in the reducer. So you can gain access to it by state.userType
Everything you need is within your context.
The only changes I would make is spread the data instead of trying to access it one at a time something like this
<AppContext.Provider value={{ ....state, dispatch }}>
then use const context = useContext(AppContext) within the component you need to access the data.
context.userType
I have this Action
export const UserIsLoggedIn = isLoggedIn => (
{ type: types.USER_IS_LOGGED_IN, isLoggedIn });
this actionConstant
export const USER_IS_LOGGED_IN = "USER_IS_LOGGED_IN";
index.js like this
import { UserIsLoggedIn } from "./redux/actions";
getUser = () => {
this.authService.getUser().then(user => {
if (user) {
this.props.UserIsLoggedIn(true);
}
}
}
const mapDispatchToProps = {
UserIsLoggedIn
}
export default connect(mapStateToProps, mapDispatchToProps) (Index);
so eventually with above code I get this.props.UserIsLoggedIn is not a function error, if I do UserIsLoggedIn(true); nothing happens... I don't quite understand where the problem is..
within the redux chrome extension I can dispatch with below without an error:
{
type: "USER_IS_LOGGED_IN", isLoggedIn : true
}
below is generally how index looks like
const store = configureStore();
class Index extends Component {
componentWillMount() {
this.getUser();
}
getUser = () => {
this.authService.getUser().then(user => {
if (user) {
console.log(this.props.isUserLoggedIn);
toastr.success("Welcome " + user.profile.given_name);
} else {
this.authService.login();
}
});
};
render() {
return (
<Provider store={store}>
<BrowserRouter>
<Route path="/" exact component={App} />
<Route path="/:type" component={App} />
</BrowserRouter>
</Provider>
);
}
}
function mapStateToProps(state) {
return {
isUserLoggedIn : state.User.isLoggedIn
}
}
const mapDispatchToProps = {
UserIsLoggedIn
}
export default connect(mapStateToProps, mapDispatchToProps) (Index);
ReactDOM.render(<Index />, document.getElementById("root"));
serviceWorker.unregister();
Note: Another aspect, mapStateToProps is not working either....
<Provider store={store}> needs to be wrapped around wherever this exported component is used. It can't be within the render method of Index. The way it is now, the connect method won't have access to your store.
You need something like:
ReactDOM.render(<Provider store={store}><Index /></Provider>, document.getElementById("root"));
and then remove the Provider portion from the render method in Index:
render() {
return (
<BrowserRouter>
<Route path="/" exact component={App} />
<Route path="/:type" component={App} />
</BrowserRouter>
);
}
You need to dispatch your action
const mapDispatchToProps = dispatch => ({
UserIsLoggedIn: (value) => {
dispatch(UserIsLoggedIn(value));
}
});
Update:
If you want to use the object syntax you need to wrap that action in a funciton:
const mapDispatchToProps = {
UserIsLoggedIn: (value) => UserIsLoggedIn(value),
};
Thank you #Yusufali2205 and #RyanCogswell for your help but those didn't fix the problem..
For people looking for an answer on this:
index.js is the first file in my React to load then App.js. With this setup, I have <Provider store={store}> inside index.js and inside index.js I don't have an access to the store within render method OR any lifecycles such as willmount or even didmount or willunmount. To access store, create it with <Provider store={store}> inside index.js and access store items within App.js.
About Dispatching action error, I still get Objects are not valid as a React child (found: object with keys {type, isLoggedIn}). If you meant to render a collection of children, use an array instead. error if I try to dispatch within render method with this code {this.props.UserIsLoggedIn(true)}.. I was attempting to do this just to see if dispatching worked, I don't plan to have it actually inside render. When I wrapped it with console.log, it worked fine, like this {console.log(this.props.UserIsLoggedIn(true))}..
When I moved these dispatchers to under a lifecycle method, they worked fine without console.log wrapper... like this
componentWillMount() {
console.log(this.props.isUserLoggedIn)
this.props.UserIsLoggedIn(true)
}
I am trying to figure out a way to store the authentication state of a user inside the redux store. Suppose isAuthenticated store the state of user if they are logged-in or not. Now, I have a cookie(httpOnly) sent by the server which remembers the user, so that they don't need to enter there credentials every time they visit the app.
Flow: User some day logged in to the application and didn't logged out and closed the browser. Now, he returns and visit my app. Since, the cookie was there in browser, this will be sent automatically(without user interaction) by the application and if the cookie is valid, the isAuthenticated: true. Very simple requirement.
Tracking the authentication status should be the first thing done by the application, so I put that logic at very first, before the App.js renders.
class App extends Component {
store = configureStore();
render() {
return (
<Provider store={this.store}>
<ConnectedRouter history={history}>
<>
<GlobalStyle />
<SiteHeader />
<ErrorWrapper />
<Switch>
<PrivateHomeRoute exact path="/" component={Home} />
<Route exact path="/login" component={LoginPage} />
<PrivateHomeRoute path="/home" component={Home} />
........code
}
This is the configureStore()
export const history = createBrowserHistory();
const configureStore = () => {
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
rootReducer(history),
composeEnhancers(applyMiddleware(sagaMiddleware, routerMiddleware(history)))
);
sagaMiddleware.run(rootSaga);
store.dispatch({ type: AUTH.AUTO_LOGIN });
return store;
};
store.dispatch({ type: AUTH.AUTO_LOGIN }); is the code where I am trying the application to do the auto-login as the first operation in the application. This action is handled by a redux-saga
function* handleAutoLogin() {
try {
const response = yield call(autoLoginApi);
if (response && response.status === 200) {
yield put(setAuthenticationStatus(true));
}
} catch (error) {
yield put(setAuthenticationStatus(false));
}
}
function* watchAuthLogin() {
yield takeLatest(AUTH.AUTO_LOGIN, handleAutoLogin);
}
autoLoginApi is the axios call to the server which will carry the cookie with it. setAuthenticationStatus(true) is action creator which will set the isAuthenticated to true false.
So, yes this is working BUT not as expected. Since, the app should first set the isAuthenticated first and then proceed with the App.js render(). But, since setting the isAuthenticated take some seconds(api call), the application first renders with the isAuthenticated: false and then after the AUTH.AUTO_LOGIN gets completed, then the application re-render for authenticaed user.
What's the problem then? For the normal component it may not be the problem, e.g this SiteHeader component
class SiteHeader extends React.Component {
render() {
const { isLoggedIn } = this.props;
if (isLoggedIn === null) {
return "";
} else {
if (isLoggedIn) {
return (
<LoggedInSiteHeader />
);
} else {
return (
<LoggedOutSiteHeader />
);
}
}
}
}
const mapStateToProps = ({ auth, user }) => ({
isLoggedIn: auth.isLoggedIn,
});
export default connect(
mapStateToProps,
null
)(SiteHeader);
But, this solution doesn't work for the Custom routing.
const PrivateHomeRoute = ({ component: ComponentToRender, ...rest }) => (
<Route
{...rest}
render={props =>
props.isLoggedIn ? (
<ComponentToRender {...props} />
) : (
<Redirect to="/login" />
)
}
/>
);
const mapStateToProps = auth => ({
isLoggedin: auth.isLoggedIn
});
export default connect(
mapStateToProps,
null
)(PrivateHomeRoute);
PrivateHomeRoute gets resolved before the redux store gets updated, hence the Route always goes to "/login".
I am looking for a solution, where the application doesn't proceed further until the authentication action doesn't complete. But, I am no clue what and where to put that code?
Few things I tried:
async await on configureStore() - Error came
async await on App.js - Error
PS: Libraries I am using redux, redux-saga,react-router-dom, connected-react-router, axios
One way I figured out:
Create a separate component MyRouteWrapper which will return the routes based on the isLoggedIn status. To, resolve the issue I stop the routes to render until the auto-login changes the isLoggedIn state.
I set the default state of isLoggedIn to null. Now, if the state is null the MyRouteWrapper will return an empty string, and once the state gets changes to true/false, it will return the routes, and then respective components get rendered.
I changed my App.js
const store = configureStore();
class App extends Component {
render() {
return (
<Provider store={store}>
<ConnectedRouter history={history}>
<MyRouteWrapper />
</ConnectedRouter>
</Provider>
);
}
}
export default App;
The component which make sure to return the Route only when the state gets changed to true/false
const MyRouteWrapper = props => {
if (props.isLoggedIn === null) {
return "";
} else {
return (
<>
<GlobalStyle />
<SiteHeader />
<ErrorWrapper />
<Switch>
<ProtectedHomeRoute
exact
path="/"
component={Home}
isLoggedIn={props.isLoggedIn}
/>
<Route path="/profile/:id" component={Profile} />
<Route path="/login" component={LoginPage} />
</Switch>
</>
);
}
};
const mapStateToProps = ({ auth }) => ({
isLoggedIn: auth.isLoggedIn
});
export default connect(mapStateToProps)(MyRouteWrapper);
This solved the issue.
I am still curious to know the solutions(better) anyone have in there mind.
When I run the code with this.props.addChannel(payload);
Channel Component keeps re-rendering like its in an infinity loop.
When I replace it with console.log(payload) it works fine.
const mapStateToProps = state => ({
user: state.shared.user,
});
const mapDispatchToProps = dispatch => ({
addChannel: (payload) => dispatch({type:ADD_CHANNEL, payload})
});
class Channel extends Component{
componentDidMount(){
const payload = api.Channels.getAll();
this.props.addChannel(payload);
//console.log("Channels", payload)
}
render(){
return(
<div>
<AddChannel />
<ChannelList channels={[{text:"test"}]} />
</div>
);
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Channel);
The api code:
const Channels = {
getAll: () => requests.get('channels/twitter/')
};
The Reducer:
import {ADD_CHANNEL} from '../constants/ActionTypes';
export default (state={}, action={}) => {
switch(action.type){
case ADD_CHANNEL:
return {...state};
default:
return {...state};
}
};
The Routes Component:
import React,{Component} from 'react';
import {Route, Switch} from 'react-router-dom';
import { connect } from 'react-redux';
import Auth from './containers/Auth';
import Channel from './containers/Channel';
import Messages from './containers/Messages';
import withAuth from './components/Auth/WithAuth';
const mapStateToProps = state => ({
user: state.shared.user,
});
class Routes extends Component{
render(){
const user = this.props.user;
return (
<Switch>
<Route exact path='/' component={Auth} />
<Route path='/messages' component={withAuth(Messages, user)} />
<Route exact path='/channels' component={withAuth(Channel, user)} />
</Switch>
);
}
};
export default connect(mapStateToProps, ()=>({}),null,{pure:false})(Routes);
The reason for the loop is probably the call to your higher-order component withAuth in the component prop of your Route's. (see Route component docs)
This call will return a new component each time Routes is rendered, which will mount a fresh Channel with an accompanying api call and redux store update. Because of {pure: false}, the store update will then trigger a rerendering of Routes (even though user hasn't changed) and start a new cycle of the loop.
If you drop {pure: false} (which doesn't seem useful here) you'll probably end the loop, but the Channel component will still do unnecessary re-mounting if one of its ancestors rerenders, resetting all local component state in Channel and below.
To fix this, you could refactor withAuth to get user as a prop rather than a parameter, and call it on the top level, outside the Routes class:
const AuthMessages = withAuth(Messages);
const AuthChannel = withAuth(Channel);
Now you can pass user to these components by using the render prop of Route:
<Route path='/messages' render={(props) => <AuthMessages {...props} user={user}/>}/>
<Route exact path='/channels' render={(props) => <AuthChannel {...props} user={user}/>}/>
Besides this, you will probably want to keep the channels in the store and handle the api call asynchronously, but I assume this code is more of a work in progress.