I know that one should generally have functional stateless components in Redux. However, I have quite a nice react statefull component and try to redesign it to work with Redux. Why? I do not want to use store to handle every bit of this componet logic while this component does its job fine; besides I'd like to keep lifycle methods. The problem I have is that do not see way to dispatch state of this component.
class LoginX extends React.Component {
constructor(props) {
super(props);
this.state = { login: '', password: '' };
}
handleChange = (e) => {
this.setState({
[e.currentTarget.name]: e.currentTarget.value,
});
}
render(){...}
}
That is simplified class, I removed 90% to keep it readable.
const mapStateToProps = (state) => {
return state;
}
const mapDispatchToProps = (dispatch) => {
return {
onClick: (e) => {e.preventDefault(); console.log('dispatch'); dispatch({
type: 'LOGIN_SUBMIT',
payload: {
token: {...LoginX.state}
}
})}
}};
let Login = connect(mapStateToProps, mapDispatchToProps)(LoginX);
export default Login;
That is rest of code. You see, I have written token: {...LoginX.state} to indicate what I wanna have rather. Yes, I know that Redux components should be functional, but maybe there is workaround for case like mine?
It's not wrong per se to define React components as classes. If the React version you are using does not support hooks and you are using features that only classes provide (state and component lifecycles) you are fine defining your components as classes.
If you want to dispatch an action with some data that is present in your component, you should provide the data from the component itself when calling the action creator.
In your case, I would do something like:
class LoginX extends React.Component {
constructor(props) {
super(props);
this.state = { login: '', password: '' };
}
handleChange = (e) => {
this.setState({
[e.currentTarget.name]: e.currentTarget.value,
});
}
onClick = (e) => {
const { login, password } = this.state;
e.preventDefault();
this.props.onSubmit({ login, password });
}
render() {
...
<button onClick={this.onClick}>submit</button>
}
}
const mapStateToProps = (state) => {
return state;
}
const mapDispatchToProps = (dispatch) => {
return {
onSubmit: (credentials) => {
console.log('dispatch');
dispatch({
type: 'LOGIN_SUBMIT',
payload: {
token: credentials
}
})
}
}
};
let Login = connect(mapStateToProps, mapDispatchToProps)(LoginX);
export default Login;
Related
EDIT: I think the issue is with my reducers
I only have one reducer called "filmsReducer" where I do this at the end :
export default combineReducers({
films: filmsReducer
});
I'm doing an app in React Native using Redux,
I want to get the initialState values below in a component :
const initialState = {
name: "",
likedFilms: [299534, 49530, 629],
dislikedFilms: [100241, 559969]
};
const filmsReducer = (state = initialState, action) => {
const { likedFilms, dislikedFilms } = state;
switch (action.type) {
case ADD_FILM:
if (action.array === "like") {
const newLikedFilms = [...state.likedFilms, action.payload];
return {
...state,
likedFilms: newLikedFilms
};
} else {
const newDislikedFilms = [...state.dislikedFilms, action.payload];
return {
...state,
dislikedFilms: newDislikedFilms
};
}
default:
return state;
}
};
And here's the component, I want to get likedFilms array from the redux state in the props of this component, but the console log doesn't work :
class LikedScreen extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
console.log(this.props.likedFilms); <-- doesn't work
}
}
const mapStateToProps = state => ({
likedFilms: state.likedFilms
});
export default connect(mapStateToProps)(LikedScreen);
Regarding your comment, you probably have to adapt your code to the following:
Edit Regarding another comment of yours, you need to change it to films instead of FilmsReducer:
const mapStateToProps = state => ({
likedFilms: state.films.likedFilms
});
It will be like, use reducer name as while mapping props in Component
class LikedScreen extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
console.log(this.props.likedFilms); <-- doesn't work
}
}
const mapStateToProps = state => ({
likedFilms: state.films.likedFilms
});
export default connect(mapStateToProps)(LikedScreen);
I have a simple form in react-redux meant to try to add a user to the database, if it is successful, display a success message. However I am not sure of the best approach to do this. I have the following:
onSubmit = e => {
...
const newUser = { user object here }
this.props.registerUser(newUser);
}
in componentWillReceiveProps(nextProps):
if (nextProps.success === true) {
this.setState({ success: nextProps.success });
}
in the render():
Meant to display a success component giving further information. There is also a conditional check to hide the form if success is true
{ this.state.success === true &&
(<SuccessComponent name={this.state.name} />)
}
in mapStateToProps:
const mapStateToProps = state => ({
success: state.success
});
in my action:
.then(res => {
dispatch({
type: REGISTRATION_SUCCESS,
payload: true
});
})
in the reducer:
const initialState = false;
export default function(state = initialState, action) {
switch (action.type) {
case REGISTRATION_SUCCESS:
return action.payload;
default:
return state;
}
}
in combineReducers:
export default combineReducers({
success: successReducer
});
In this, I am basically using the response from the server to dispatch a success prop to the component, update the state, which forces react to render and go through the conditional statement again, hiding the form and displaying the new success block.
However, when I go into redux dev tools, I see that the state from the store is now true, and remains so should users navigate away. Is there a better way to go about this objective? I find that maybe this should be isolated to component state itself, but not sure how to do it since the action and hence the server response is through redux.
Redux is a state machine, not a message bus, so try to make your state values represent the current state of the application, not to send one-time messages. Those can by the return value of the action creator. Success or failure can simply be the existence/lack of an error from the action creator.
If you actually do want to store the user info, you can derive your "was successful" state by virtue of having a registered user, and clear out any existing registered user on component mount.
// actions.js
export const clearRegisteredUser = () => ({
type: SET_REGISTERED_USER,
payload: null,
})
export const register = (userData) => async (dispatch) => {
// async functions implicitly return a promise, but
// you could return it at the end if you use the .then syntax
const registeredUser = await api.registerUser(userData)
dispatch({
type: SET_REGISTERED_USER,
payload: registeredUser,
})
}
// reducer.js
const initialState = { registeredUser: null }
const reducer = (state = initialState, { type, payload }) => {
switch(type) {
case SET_REGISTERED_USER: {
return {
...state,
registeredUser: payload,
}
}
default: {
return state
}
}
}
// TestComponent.js
class ExampleComponent extends Component {
state = {
registrationError: null,
}
componentWillMount() {
this.props.clearRegistered()
}
handleSubmit = async (formData) => {
try {
this.props.register(formData)
} catch(error) {
// doesn't really change application state, only
// temporary form state, so local state is fine
this.setState({ registrationError: error.message })
}
}
render() {
const { registrationError } = this.state
const { registeredUser } = this.props
if (registrationError) {
return <FancyError>{registrationError}</FancyError>
} else if (registeredUser) {
return <SuccessComponent name={this.props.registeredUser.name} />
} else {
return <UserForm onSubmit={this.handleSubmit} />
}
}
}
If you really don't need the user info after you register, you can just perform the api call directly and leave Redux out of it.
class ExampleComponent extends Component {
state = {
registered: false,
registrationError: null,
}
handleSubmit = async (formData) => {
try {
await api.registerUser(formData)
this.setState({ registered: true })
} catch(error) {
this.setState({ registrationError: error.message })
}
}
render() {
const { registered, registrationError } = this.state
if (registrationError) {
return <FancyError>{registrationError}</FancyError>
} else if (registered) {
return <SuccessComponent name={this.props.registeredUser.name} />
} else {
return <UserForm onSubmit={this.handleSubmit} />
}
}
}
Finally, avoid keeping copies of redux state in your component state whenever possible. It can easily lead to sync issues. Here's a good article on avoiding derived state: https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html
First off, I don't think that you should be using your Redux store for saving what is essentially local state.
If it was me I would probably try to make the api call directly from the component and then write to the redux store if it is successful. That way you could avoid having your derived state in the component.
That said, if you want to do it this way I would suggest componentWillUnmount. That would allow you have another Redux call that would turn your registration boolean back to false when you leave the page.
I would like to use localstorage to persist the auth state to avoid slow page content on refreshes. I've read about this elsewhere but unsure how to implement in my case. Could anyone help me work out how to edit the below to make it work please?
This example is similar but I'm not sure how to apply it to my case.
import React from 'react';
import { firebase } from '../firebase';
import PropTypes from 'prop-types';
const withAuthentication = (Component) => {
class WithAuthentication extends React.Component {
constructor(props) {
super(props);
this.state = {
authUser: null,
};
}
getChildContext() {
return {
authUser: this.state.authUser,
};
}
componentDidMount() {
firebase.auth.onAuthStateChanged(authUser => {
authUser
? this.setState(() => ({ authUser }))
: this.setState(() => ({ authUser: null }));
});
}
render() {
return (
<Component />
);
}
}
WithAuthentication.childContextTypes = {
authUser: PropTypes.object,
};
return WithAuthentication;
}
export default withAuthentication;
Easy, just replace:
this.setState(() => ({ authUser }))
with
localStorage.setItem('authUser', JSON.stringify(authUser))
or
localStorage.removeItem('authUser')
to remove it
then you can read it:
JSON.parse(localStorage.getItem('authUser'))
instead of this.state.authUser
and in componentDidMount check if localStorage.getItem('authUser') exists before making the call again.
I am trying to implement auth (sign up/out) using React + Redux (SSR and Thunks). I have no idea why components are not updatating when Redux state updates...
This is the component that should get rerendered:
class Navbar extends React.Component {
constructor(props) {
super(props);
this.state = {
loggedIn: props.authentication.loggedIn
};
}
render() {
let links = null;
if (this.state.loggedIn) {
links = ...
} else {
links = ...
}
return (<Toolbar>{links}</Toolbar>)
}
}
const mapStateToProps = state => {
return {
authentication: state.authentication
}
}
const mapDispatchToProps = dispatch => {
return {
signOut: () => {dispatch(userActions.logout())}
}
}
const AuthNavbar = connect(mapStateToProps, mapDispatchToProps)(Navbar)
export default AuthNavbar;
And that is my reducer:
const reducers = {
authentication,
registration,
alert
}
const todoApp = combineReducers(reducers)
export default todoApp
Authentication reducer:
const authentication = (state = initialState, action) => {
switch (action.type) {
...
case userConstants.LOGIN_SUCCESS:
return Object.assign({}, state, {
loggedIn: true,
loggingIn: false,
user: action.user
});
...
default:
return state;
}
}
And The Action - Login:
function login(email, password) {
return dispatch => {
dispatch({type: userConstants.LOGIN_REQUEST, email});
userService.login(email, password).then(
user => {
dispatch({type: userConstants.LOGIN_SUCCESS, user});
},
error => {
dispatch({ type: userConstants.LOGIN_FAILURE });
dispatch({type: alertActions.error(error)});
}
);
}
}
UserService.login is a function that calls and api witch fetch.
Looks like Action gets fired as it should, Redux state gets updated, but the component does not update:
Double checked Redux Dev Tools - state does get updated, so there must be a problem with the way I am using connect utilities?
You are storing the logedin props in the state inside the constructor, which will run only once in the life time of the component.
When a new prop is coming back you are not updating the state.
Either use the props directly:
if (this.props.authentication.loggedIn) {
links = ...
Or update the state in componentWillReceiveProps
componentWillReceiveProps(nextProps){
// update the state with the new props
this.setState({
loggedIn: nextProps.authentication.loggedIn
});
}
Your render function is dependent on state.loggedIn, but state.loggedIn is not changing; only this.props.authentication.loggedIn is changing in response to the action. Your component, in its current form, does not need state. You can remove it to make this work:
class Navbar extends React.Component {
render() {
let links = null;
if (this.props.authentication.loggedIn) {
links = ...
} else {
links = ...
}
return (<Toolbar>{links}</Toolbar>)
}
}
I am new to react native + redux. I have an react native application where user first screen is login and after login am showing page of list of categories from server. To fetch list of categories need to pass authentication token, which we gets from login screen or either if he logged in previously then from AsyncStorage.
So before redering any component, I am creating store and manully dispatching fetchProfile() Action like this.
const store = createStore(reducer);
store.dispatch(fetchProfile());
So fetchProfile() try to reads profile data from AsyncStorage and dispatch action with data.
export function fetchProfile() {
return dispatch => {
AsyncStorage.getItem('#myapp:profile')
.then((profileString) => {
dispatch({
type: 'FETCH_PROFILE',
profile: profileString ? JSON.parse(profileString) : {}
})
})
}
}
so before store get populated, login page get rendered. So using react-redux's connect method I am subscribing to store changes and loading login page conditionally.
class MyApp extends React.Component {
render() {
if(this.props.profile)
if(this.props.profile.authentication_token)
retunr (<Home />);
else
return (<Login />);
else
return (<Loading />);
}
}
import { connect } from 'react-redux';
const mapStateToProps = (state) => {
return {
profile: state.profile
}
}
module.exports = connect(mapStateToProps, null)(MyApp);
So first 'Loading' component get rendered and when store is populated then either 'Login' or 'Home' component get rendered. So is it a correct flow? Or is there a way where I can get store populated first before any compnent render and instead of rendering 'Loading' component I can directly render 'Login' or 'Home' Component.
Verry common approach is to have 3 actions for an async operation
types.js
export const FETCH_PROFILE_REQUEST = 'FETCH_PROFILE_REQUEST';
export const FETCH_PROFILE_SUCCESS = 'FETCH_PROFILE_SUCCESS';
export const FETCH_PROFILE_FAIL = 'FETCH_PROFILE_FAIL';
actions.js
import * as types from './types';
export function fetchProfile() {
return dispatch => {
dispatch({
type: types.FETCH_PROFILE_REQUEST
});
AsyncStorage.getItem('#myapp:profile')
.then((profileString) => {
dispatch({
type: types.FETCH_PROFILE_SUCCESS,
data: profileString ? JSON.parse(profileString) : {}
});
})
.catch(error => {
dispatch({
type: types.FETCH_PROFILE_ERROR,
error
});
});
};
}
reducer.js
import {combineReducers} from 'redux';
import * as types from './types';
const isFetching = (state = false, action) => {
switch (action.type) {
case types.FETCH_PROFILE_REQUEST:
return true;
case types.FETCH_PROFILE_SUCCESS:
case types.FETCH_PROFILE_FAIL:
return false;
default:
return state;
}
};
const data = (state = {}, action) => {
switch (action.type) {
case types.FETCH_PROFILE_SUCCESS:
return action.data;
}
return state;
};
export default combineReducers({
isFetching,
data
});
So you can get isFetching prop in your component and show/hide Loader component
You can load all your data during the splash screen and then load the others screens after that. I did it like this. Hope it helps
class Root extends Component {
constructor(props) {
super(props);
this.state = {
isLoading: true,
store: configureStore( async () => {
const user = this.state.store.getState().user || null;
if (categories && categories.list.length < 1) {
this.state.store.dispatch(categoriesAction());
}
this.setState({
isLoading: false
});
}, initialState)
};
}
render() {
if (this.state.isLoading) {
return <SplashScreen/>;
}
return (
<Provider store={this.state.store}>
<AppWithNavigationState />
</Provider>
);
}
}
Redux and Redux Persist (https://github.com/rt2zz/redux-persist) will solve your problem.
Don't make them complex.