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.
Related
I have a component PostsShow which is showing the selected post:
#connect((state) => ({post: state.posts.post}), {fetchPost})
class PostsShow extends Component {
componentDidMount() {
this.props.fetchPost(this.props.match.params.id);
}
render() {
const { post } = this.props;
if (!post) {
return <div></div>;
}
return (
<div>
<Link to='/'>Back</Link>
<h3>{post.title}</h3>
<h6>Categories: {post.categories}</h6>
<p>{post.content}</p>
</div>
);
}
}
The problem is when user first visits the page, fetchPost function populates state section (posts.post) with some data associated with chosen post and when the user chooses another post, he can see old data for 1-2 sec. (before new request is finished).
Actions map:
Click on post #1
Click Back button
Click on post #2
For 1 sec. you can see old (#1) post, until the request is finished and component refreshed with the post (#2) data.
I'm new to whole redux concept, so i'm curious how are you avoiding this kind of behavior?
MY SOLUTION:
I assume that you can create a switch branch, which will modify sate with (posts.post part) with null value and trigger this behavior on componentWillUnmount method. So:
Action:
export function clearPost() {
return {
type: CLEAR_POST,
payload: null
}
}
Reducer:
const INITIAL_STATE = { all: [], post: null };
export default function (state = INITIAL_STATE, action) {
switch (action.type) {
// ... Other cases
case CLEAR_POST:
return { ...state, post: null }
default:
return state;
}
}
Component:
class PostsShow extends Component {
componentDidMount() {
this.props.fetchPost(this.props.match.params.id);
}
componentWillUnmount() {
this.props.clearPost();
}
render() {
// Old render
}
}
Is this a good approach for react with redux?
Your state structure is not ideal. Try keeping your posts like that:
posts: {
byID: {
1: {/*the post* no 1/},
2: {/*the post* no 2/},
// ...
}
allIDs: [2, 1 /*, ...*/],
}
This way you can provide an ordered list of post id's for a list view and show a single post by getting it from the state like: this.posts.byID['thePostID'].
Also read up in the redux docs on how to normalize state.
This will also fix your problem because when your get your post from the store with an id that does not already exist, it will be undefined thus rendering as an empty div. A loading indicator would be the best thing to show.
This is because you are making an async call , which takes time to get new date, that is why when fetch posts gets new data then your component will update and render it.
You can have two approaches:
1) you can create a an async function which will dispatch an action that will have a update the reducer with payload as
status :LOADING,
once the fetch function returns with new data again dispatch an action that
will update the reducer as
status:SUCCESS
and in your component check if status received in the props by store is 'LOADING' or 'SUCCESS', if SUCCESS then show new state, if LOADING keep showing some text like component is Loading:
here is the example
SomeComponent.js
export default class SomeComponent extends Component {
constructor (props) {
super(props)
this.model = {}
}
render () {
const {
status
} = this.props
const loading = status === 'LOADING'
return (
<Layout>
<Row>
<FormField
label='First Name'
id='first-name'
value={details.firstName}
onChange={this.update('firstName’)}
disabled={loading}
mandatory
/>
</Row>
</Layout>
)
}
}
actions.js
const fetchDetails = (someId) => {
return (dispatch) => {
dispatch(dataFetching())
return http.get(dispatch, `https//someCall`, null, {
'Accept': SOME_HEADER
}).then((data) => {
dispatch(dataFetched(data))
}).catch((error) => {
dispatch(someError(`Unable to retrieve the details ${error}`))
})
}
}
const dataFetching = () => ({
type: constants.DATA_FETCHING
})
const dataFetched = (data) => ({
type: constants.DATA_FETCHED,
data
})
Reducer.js
export default function reducer (state = {}, action = {}) {
switch (action.type) {
case constants.DATA_FETCHING:
return Object.assign({}, state, {
data: {},
status: 'LOADING'
})
case constants.DATA_FETCHED:
return Object.assign({}, state, {
data: action.data,
status: 'SUCCESS'
})
default:
return state
}
}
2nd Approach
The one which you did, but for clarity try to use the first one.
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'm new to Redux Saga, coming from Redux Thunk. In some situations, I need to know whether an API call fails or succeeds from inside the view from which I called the action. With Redux Thunk, I would do something like the following.
My component and action creator would look like this:
class MyComponent extends Component {
componentDidMount() {
this.props.actions.fetchItems()
.then(result => {
if (result.status === 'OK') {
console.log('Request succeeded, take particular action in this view')
}
else {
console.log('Request failed, take other action in this view')
}
})
}
render() {
return (
<div>My view</div>
)
}
}
const mapStateToProps = (state, ownProps) => {
return {
items: state.items,
}
}
const mapDispatchToProps = dispatch => {
return {
actions: bindActionCreators({
...actions,
}, dispatch),
}
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(MyComponent)
import * as t from './actionTypes'
import {CALL_API} from '../api'
const xhrGetItems = (params) => (dispatch, getState) => {
const action = {
[CALL_API]: {
type: t.XHR_ITEMS_FETCH,
endpoint: `http://www.example.com/myendpoint`,
method: 'get',
}
}
return dispatch(action)
}
My API middleware catches all actions with a CALL_API property, uses an ajax library to make the appropriate call, then returns a fulfilled promise. My reducer is set up to handle each of the possible states of the api call (success, failure, pending). All the while, I can still check the result of the call in my view where it originated.
So my question is, how can this be accomplished with Redux Saga? Right now my saga api middleware is doing everything it should be doing, but when I call fetchItems() in my view, the result is a plain JS object, so I can't check whether or not it succeeded.
It's possible that I'm going about this completely wrong, too. Any suggestions are very much appreciated.
A common pattern with redux and redux-saga is to create 3 Actions for an API call. In your case I would create:
LIST_ITEMS_START
LIST_ITEMS_SUCCEEDED
LIST_ITEMS_FAILED
Your saga would look something like this:
function* watchListItemsStart() {
yield takeLatest(LIST_ITEMS_START, listItemsStartFlow)
}
function* listItemsStartFlow() {
try {
const result = yield call(api.getItems)
if (result.status === 'OK') {
yield put(LIST_ITEMS_SUCCEEDED, items)
} else {
throw new Error(result.error)
}
} catch (error) {
yield put(LIST_ITEMS_FAILED, error)
}
}
The sagas have cleanly abstracted away the API side-effect. The reducer can concentrate on state management:
switch (action.type) {
case LIST_ITEMS_START:
return {
...state,
isLoading: true,
error: null,
items: [],
}
case LIST_ITEMS_SUCCEEDED:
return {
...state,
isLoading: false,
items: action.payload.items,
}
case LIST_ITEMS_FAILED:
return {
...state,
isLoading: false,
error: action.payload.error.getMessage(),
}
}
Now you have everything you need in your store that can be selected and reacted on in the component.
class MyComponent extends Component {
componentDidMount() {
this.props.fetchItems()
}
render() {
const { isLoading, items, error } = this.props
// Here you can react to the different states.
return (
<div>My view</div>
)
}
}
connect(state => ({
isLoading: itemsSelectors.isLoading(state),
items: itemsSelectors.getItems(state),
error: itemsSelectors.getError(state),
}), {
fetchItems: actions.fetchItems
})(MyComponent)
An important point here is, that your component gets very dumb. It just gets props and does not handle the result of the API call (no promise.then here).
I hope this answer will help you.
Say I have a redux module that looks like this:
import fetch from 'isomorphic-fetch';
// CONSTANTS
const LOAD = 'LOAD';
const LOAD_SUCCESS = 'LOAD_SUCCESS';
// REDUCER
const initialState = { loading: false, data: [] };
export default function reducer(state = initialState, action) {
switch (action.type) {
case LOAD: return { ...state, loading: true };
case LOAD_SUCCESS: return { ...state, loading: false, data: action.data };
default: return state;
}
}
// ACTION CREATORS
function requestLocations() {
return { type: LOAD };
}
function receiveLocations(json) {
return { type: LOAD_SUCCESS, data: json };
}
export function fetchLocations() {
return function (dispatch) {
dispatch(requestLocations());
return fetch('http://myurl')
.then(response => response.json())
.then(json => dispatch(receiveLocations(json)));
};
}
I'm struggling with the loading state on the first render if I make the async call in componentWillMount. Imagine my component looks like this (simplified for brevity):
export default class LocationContainer extends React.Component {
componentWillMount() {
fetchLocations(); // already bound and injected via connect.
}
render() {
const { loading, data } = this.props; // injected via connect from reducer above.
if (loading) {
return <Spinner />;
} else {
return <LocationExplorer locations={ data } />;
}
}
}
The problem I run into is that on the first render of LocationContainer, loading is false and data hasn't been fetched yet. In componentWillMount, the LOAD action is fired and a props change of loading being set to true is queued up to happen on the subsequent render. In the meantime, during my first render, LocationExplorer is rendered when I really wanted Spinner instead because loading is still false. I'm wondering how you deal with this without setting a firstRender = true state variable hack.
One option could be extend your loading condition with your data initial state:
const initialState = { loading: false, data: [] };
When you are loading and your data is empty it means you are in this exact state of waiting for new data to come:
if (loading && data.length === 0) {
return <Spinner />;
} else {
Also, I usually put my asynchronous calls in componentDidMount instead of componentWillMount.
Don't make the request in componentWillMount. Instead, do it before you mount the component. e.g.
store.dispatch(fetchLocations());
ReactDOM.render(<App store={store} />, mountpoint);
Besides the accepted answer, one other solution we've been using to great success is adding a complete boolean flag to the reducer and we mark it true if a response has come back at some point. This makes things a bit more explicit in the event the response actually comes back with an empty array. The complete flag lets me know that the response is from the server and not just my initial state.
if(loading && !complete) {
return <Spinner />;
} else {
return <Component data={data} />;
}
I figured I would add this just in case it's helpful to others.
Completely new to React/Redux, and pretty confused about all the different ways to create components etc. I have read the async docs for Redux and managed to get async data loaded as the page loads, but now I want to use a button to trigger data download.
So far I have a components with this in the render function.
<button onClick={() => dispatch(getEntry('dummy'))}> Get some data< /button>
This is reaching my action creator
export function getEntry(apiroute) {
return {
type: GET_ENTRY,
apiroute
};
}
and redux is passing this to my reducer
export function entry(state = initialState, action) {
switch (action.type) {
case 'GET_ENTRY':
return Object.assign({}, state, {
fetching : true
});
I can see in my logs that the state is being changed by the action.
In my actions file I have this code, which I know works if I wire it into to bootstrapping of my app.
export function fetchEntry() {
return function(dispatch) {
return window.fetch('/google.json')
.then(response => response.json())
.then(json => dispatch(recEntry(json)));
}
}
But where and how should I call fetchEntry in the sequence above following the button click?
The idiomatic way to do async is to use something like redux-thunk or redux-promise. redux-thunk I think is more common though. You used the thunk pattern in your fetchEntry function, but I don't think idiomatically. From the top:
Your button code looks good.
Your getEntry action creator is a bit of a misnomer. You're using it more as initiateGetEntry, right?
Using redux-thunk, you'd combine the two action creators into a thunk action creator:
export function getEntry() {
return function(dispatch) {
dispatch({ type: 'GET_ENTRY_INITIATE' });
fetch('google.json)
.then(function(res) { dispatch({ type: 'GET_ENTRY_SUCCESS, payload: res }) }
.catch(function(res) { dispatch({ type: 'GET_ENTRY_FAIL' });
}
}
The reducer would be similar to what you had:
export function entry(state = initialState, action) {
switch (action.type) {
case 'GET_ENTRY_INITIATE':
return Object.assign({}, state, { fetching : true });
case 'GET_ENTRY_SUCCESS':
return Object.assign({}, state, action.payload, { fetching: false });
case 'GET_ENTRY_FAIL':
return Object.assign({}, state, { fetching: false, error: 'Some error' });
default:
return state;
}
Then, your UI code works fine:
<button onClick={() => dispatch(getEntry('dummy'))}> Get some data< /button>
OK, not sure whether this is the only way, but this worked in my Component
constructor(props) {
super(props);
this.getdata = this.getdata.bind(this);
}
getdata(evt) {
const { dispatch } = this.props;
dispatch(getEntry('getdata'));
fetchEntry()(dispatch);
}
render() {
return (
<div>
<button onClick={this.getdata}> Get some data2</button>
Basically I dispatch a message to the store that an update is starting, and then start the update.
Would welcome confirmation that this is an idiomatic pattern.