I am using NGRX. I have many actions and I want to know when actions start and when the store is updated.
The idea is to have a centralized way to get the information no matter what action is executed. I need to know when the store updates without subscribing to all selectors.
updateTitle ----> title is updated.
best,
Hmendez
You have to explicitly define actions
e.g.
import { createAction, props } from "#ngrx/store";
export enum HeaderActionTypes {
UpdateTitle = '[Title] Update Title',
UpdateTitleSuccess = '[Title] Update Title success'
}
export const UpdateTitle = createAction(HeaderActionTypes.UpdateTitle)
export const UpdateTitleSuccess = createAction(HeaderActionTypes.UpdateTitleSuccess, props<{ payload: string }>())
in reducer you can catch actions and update the state
e.g
import { createReducer, on } from "#ngrx/store";
export const initialState = {
... // Additional state properties can go here.
updatingTitle: false,
title: ''
}
export const reducer = createReducer(
initialState,
on(HeaderActionTypes.UpdateTitle, (state) => {
return {
...state,
updatingTitle: true
}
}),
on(HeaderActionTypes.UpdateTitleSuccess, (state, { payload }) => {
return {
...state,
updatingTitle: false,
title: payload
}
})
)
If title is getting updated with async call you have to add an Effect
Effect, Checkout the documentation.
Selectors, Use selectors to read state and bind it to UI.
Related
I managed to write reducer using createSlice but the action seems to be confusing.
My old reducer :
function listPeopleReducer(state = {
getPeople:{}
}, action){
switch (action.type) {
case D.LIST_PEOPLE: {
return {
...state
, getPeople:action.payload
}
}
default:{}
}
return state
}
By using createSlice from the redux toolkit, I migrated the reducer to this,
const listPeopleReducer = createSlice({
initialState:{getPeople:{}},
name:"listPeople",
reducers:{
listPeople(state,action){
return {
...state,
getPeople : action.payload
}
}
}
})
My old action, makes an api call inside it, with the help of a helper function makeApiRequest (which takes in parameters and returns the response of the api),
export function listPeople(config: any) {
return function (dispatch: any) {
makeApiRequest(config)
.then((resp) => {
dispatch({
type : D.LIST_PEOPLE,
payload : resp.data
})
})
.catch((error) => {
dispatch({
type : D.LIST_PEOPLE,
payload : error
})
})
}
}
With reduxtool kit, we could do something like,
const listPeople = listPeopleReducer.actions.listPeople;
But, how will I write my custom action that contains the helper function makeApiRequest ?
i.e The old Action should be migrated to reduxtoolkit type.
It's definitely tricky when migrating, since there are some major conceptual changes that you must eventually wrap your head around. I had to do it a couple of times before it clicked.
First, when you are creating const listPeopleReducer with createSlice(), that is not actually what you are creating. A slice is a higher level object that can generate action creators and action types for you, and allows you to export reducers and actions FROM it.
Here are the changes I would make to your code:
const peopleSlice = createSlice({
initialState:{getPeople:{}},
name:"people",
reducers:{
listPeople(state,action){
// uses immer under the hood so you can
// safely mutate state here
state.getPeople = action.payload
}
},
extraReducers:
// each thunk you create with `createAsyncThunk()` will
// automatically have: pending/fulfilled/rejected action types
// and you can listen for them here
builder =>
builder.addCase(listPeople.pending, (state,action) => {
// e.g. state.isFetching = true
})
builder.addCase(listPeople.fulfilled, (state,action) => {
// e.g. state.isFetching = false
// result will be in action.payload
})
builder.addCase(listPeople.rejected, (state,action) => {
// e.g. state.isFetching = false
// error will be in action.payload
})
}
})
Then, outside of your slice definition, you can create actions by using createAsyncThunk(), and do like:
export const listPeople = createAsyncThunk(
`people/list`,
async (config, thunkAPI) => {
try {
return makeApiRequest(config)
} catch(error) {
return thunkAPI.rejectWithError(error)
// thunkAPI has access to state and includes
// helper functions like this one
}
}
}
The "Modern Redux with Redux Toolkit" page in the Redux Fundamentals docs tutorial shows how to migrate from hand-written Redux logic to Redux Toolkit.
Your makeApiRequest function would likely be used with Redux Toolkit's createAsyncThunk, except that you should return the result and let createAsyncThunk dispatch the right actions instead of dispatching actions yourself.
In a react-native, redux, firebase project, I have a drawer component that subscribes to an onSnapshot listener when the component mounts, and on will unmount, it calls the snapshot reference. this component looks like this:
import { onAccountChange } from '../actions/Agenda';
import {dispatch} from 'redux';
class DrawerContentComponent extends React.Component {
constructor (props) {
super(props);
}
componentDidMount(){
this.unsubscribeAccount = firebase.firestore().collection('users').doc(this.props.authUser.uid).onSnapshot((doc) => {
dispatch({type: types.LOAD_ACCOUNT, payload: doc.data()})
});
}
componentWillUnmount() {
this.unsubscribeAccount();
}
< ...rest of component... >
EDIT:
const mapStateToProps = ({ account, auth, inbox, agenda }) => {
const { role, profileImg, legalName, username, rating, phoneNumber } = account;
const { conversations } = inbox;
const { authUser } = auth;
const { events } = agenda;
return {
role,
profileImg,
legalName,
username,
rating,
phoneNumber,
authUser,
conversations,
events
};
};
const mapDispatchToProps = { logoutUser, onProfileChange, onAccountChange, getConversations, getAgenda };
export default connect(mapStateToProps, mapDispatchToProps)(DrawerContentComponent);
}
Edit: onAccountChange():
export const onAccountChange = (uid) => {
return (dispatch) => {
firebase.firestore().collection('users').doc(uid).onSnapshot((doc) => {
dispatch({ type: types.LOAD_ACCOUNT, payload: doc.data() });
});
};
};
The above functions as necessary, because I couldn't manage to unsubscribe from the action, which previously was placed in an external directory for actions.
Problem: I want to be able to implement this by somehow using the function thats already created in the actions file ( getAgenda()) without having to rewrite the code in the component, because im currently doing that just to have the ability to unsubscribe from the listener on unmount, only way I thought of to make it work.
ideally, id like to do something like this:
componentDidMount() {
this.unsubscribeAgenda = this.props.getAgenda();
}
componentWillUnmount() {
this.unsubscribeAgenda();
}
But the above results in:
TypeError: 'dispatch is not a function' if I take out the dispatch import, the error is ReferenceError: Cant find variable: dispatch, I obviously need to dispatch changes for a onSnapshot listener
What are some strategies to handle this?
You can't import dispatch directly from redux.
You need to either use react-redux's connect() function to wrap your action creators with dispatch or get dispatch directly from it.
If you are using a functional component, you could use useDispatch to get access to it.
If you don't want to use one of the normal react-redux options, you can export dispatch from your store, and then import it from where you created your store.
export const dispatch = store.dispatch
If most of your logic for the firestore is in an redux thunk action (or similar with asynchronous capabilities), use connect to get the action wrapped in dispatch and run it as you have in your ideal at the end. Whatever you return from a thunk action is returned from the call as well, so you should be able to set it up to return the unsubscribe function.
connect({},{onAccountChange})(DrawerContentComponent)
Then you can dispatch onAccountChange action creator using:
this.props.onAccountChange()
Edit:
Modify your onAccountChange function to this so that your thunk returns your unsubscibe function.
export const onAccountChange = (uid) => {
return (dispatch) => {
return firebase
.firestore()
.collection('users')
.doc(uid)
.onSnapshot((doc) => {
dispatch({ type: types.LOAD_ACCOUNT, payload: doc.data() });
});
};
};
Then you just need to add onAccountChange to the mapDispatch to props and use this in your componentDidMount method:
this.unsubscribeAccount = this.props.onAccountChange();
For making components to be attached to store for both dispatch actions or mapping props, it is used with connect(mapStateToProps, mapDispatchToProps)(Component). in your case, there is no props passed to component so I'll just send null for mapStateToProps
(assuming you used Provider at some parent component REDUX. I cant understand how to connect a component defined as a class extending React.Component in order to read the store)
import { connect } from 'react-redux';
class DrawerContentComponent extends React.Component {
...rest code...
componentDidMount() {
this.unsubscribeAgenda = this.props.getAgenda();
}
componentWillUnmount() {
this.unsubscribeAgenda();
}
}
export default connect(null, { getAgenda })(DrawerContentComponent)
I try to implement a async react-select (Select.Async). The problem is, we want to do the fetch in redux-saga. So if a user types something, the fetch-action should be triggered. Saga then fetches the record and saved them to the store. This works so far.
Unfortunately loadOptions has to return a promise or the callback should be called. Since the newly retrieved options get propagated with a changing property, I see no way to use Select.Async together with saga to do the async fetch call. Any suggestions?
<Select.Async
multi={false}
value={this.props.value}
onChange={this.onChange}
loadOptions={(searchTerm) => this.props.options.load(searchTerm)}
/>
I had a hack where i assigned the callback to a class variable and resolve it on componentWillReceiveProps. That way ugly and did not work properly so i look for a better solution.
Thanks
redux-saga is for handling side effects like asynchronously receiving options for react-select. That's why you should leave the async stuff to redux-saga. I have never used react-select but by just looking at the documentation I would solve it this way:
Your component gets very simple. Just get value and options from your redux store. optionsRequested is an action creator for the OPTIONS_REQUESTED action:
const ConnectedSelect = ({ value, options, optionsRequested }) => (
<Select
value={value}
options={options}
onInputChange={optionsRequested}
/>
)
export default connect(store => ({
value: selectors.getValue(store),
options: selectors.getOptions(store),
}), {
optionsRequested: actions.optionsRequested,
})(ConnectedSelect)
A saga definition watches for OPTIONS_REQUESTED action that is trigged by onInputChange, loads the data with given searchTerm from server and dispatches OPTIONS_RECEIVED action to update redux store.
function* watchLoadOptions(searchTerm) {
const options = yield call(api.getOptions, searchTerm)
yield put(optionsReceived(options))
}
In other words: Make your Component as pure as possible and handle all side-effect/async calls in redux-saga
I hope this answer was useful for you.
The main idea is that you are capable to dispatch redux actions using application context from
import React from 'react';
import { connect } from 'react-redux';
import Select from '#components/Control/Form/Skin/Default/Select';
import { reduxGetter, reduxSetter, required as req } from '#helpers/form';
import { companyGetTrucksInit } from "#reduxActions/company";
import AppContext from '#app/AppContext';
const FIELD_NAME = 'truck';
export const getReduxValue = reduxGetter(FIELD_NAME);
export const setReduxValue = reduxSetter(FIELD_NAME);
const SelectCompanyTruck = (props) => {
const {
required,
validate=[]
} = props;
const vRules = [...validate];
if (required)
vRules.push(req);
return (
<AppContext.Consumer>
{({ dispatchAction }) => (
<Select
loadOptions={(inputValue, callback) => {
function handleResponse(response) {
const { data: { items } } = response;
const options = items.map(i => ({ label: i.name, value: i.id }));
callback(options);
}
dispatchAction(companyGetTrucksInit, { resolve: handleResponse, inputValue });
}}
name={FIELD_NAME}
{...props}
/>
)}
</AppContext.Consumer>
);
}
export default SelectCompanyTruck;
I have the following two #ngrx/store reducers:
import {ActionReducer, Action} from '#ngrx/store';
import {UserAccount} from '../shared/models/useraccount.model';
export const SET_CURRENT_USER_ACCOUNT = 'SET_CURRENT_USER_ACCOUNT';
export const UPDATE_CURRENT_USER_ACCOUNT_FIRST_NAME = 'UPDATE_CURRENT_USER_ACCOUNT_FIRST_NAME';
export const currentUserAccountReducer: ActionReducer<UserAccount> = (state: UserAccount, action: Action) => {
console.log('currentUserAccountReducer:', state, action);
switch (action.type) {
case SET_CURRENT_USER_ACCOUNT: {
return action.payload;
}
case UPDATE_CURRENT_USER_ACCOUNT_FIRST_NAME: {
state.firstName = action.payload;
return state;
}
}
};
export const SET_AUTHENTICATED = 'SET_AUTHENTICATED';
export const SET_UNAUTHENTICATED = 'SET_UNAUTHENTICATED';
export const authenticatedReducer: ActionReducer<boolean> = (state: boolean, action: Action) => {
console.log('authenticatedReducer:', state, action);
switch (action.type) {
case SET_AUTHENTICATED: {
return true;
}
case SET_UNAUTHENTICATED: {
return false;
}
}
};
However, for some reason when I issue a dispatch for the 1st reducer (i.e. currentUserAccountReducer) then it changes the state for the 2rd reducer (i.e. authenticatedReducer)...
Here is the dispatch causing this issue:
this.store.dispatch({type: SET_CURRENT_USER_ACCOUNT, payload: currentUserAccount});
Here is how I initialize the store in the imports section:
StoreModule.provideStore(
{
currentUserAccount: currentUserAccountReducer,
authenticated: authenticatedReducer
})
Can someone please provide advice?
edit: The issue is that authenticated ends up undefined!!
The switch statements in your reducers do not contain default cases. You need to add default cases that return the state, as the reducers will be called for all actions - the store has no way of knowing which reducer should be called for a particular action type, so each dispatched action is passed to every reducer.
Just built my first API Middleware and was just wondering where I'm suppose to chain promises for action creators that dispatch multiple actions. Is what I did an anti-pattern:
export const fetchChuck = () => {
return {
[CALL_API]: {
types: [ CHUCK_REQUEST, CHUCK_SUCCESS, CHUCK_FAILURE ],
endpoint: `jokes/random`
}
}
}
export const saveJoke = (joke) => {
return { type: SAVE_JOKE, joke: joke }
}
export const fetchAndSaveJoke = () => {
return dispatch => {
dispatch(fetchChuck()).then((response) => {
dispatch(saveJoke(response.response.value.joke))
})
}
}
Should fetchAndSaveJoke dispatch the section action in my react component or is it okay to have it as its own action creator?
I would say that at this point in the Redux world, it's not super clear what's best practice and what the anti-patterns are. It's a very unopinionated tool. While that's been great for a diverse ecosystem to flourish, it does present challenges for people looking for ways to organize their apps without running into pitfalls or excessive boilerplate. From what I can tell, your approach seems to be roughly in line with the advice from the Redux guide. The one thing that looks funny to me is that it seems like CHUCK_SUCCESS should probably make SAVE_JOKE unnecessary.
I personally find it rather awkward to have action creators dispatch more actions, and so I worked out the approach behind react-redux-controller. It's brand new, so it's certainly not a "best practice", but I'll throw it out there in case you or someone else wants to give it a try. In that workflow, you'd have a controller method that looks something like:
// actions/index.js
export const CHUCK_REQUEST = 'CHUCK_REQUEST';
export const CHUCK_SUCCESS = 'CHUCK_SUCCESS';
export const CHUCK_FAILURE = 'CHUCK_FAILURE';
export const chuckRequest = () => { type: CHUCK_REQUEST };
export const chuckSuccess = (joke) => { type: CHUCK_SUCCESS, joke };
export const chuckFailure = (err) => { type: CHUCK_FAILURE, err };
// controllers/index.js
import fetch from 'isomorphic-fetch'; // or whatever
import * as actions from '../actions';
const controllerGenerators = {
// ... other controller methods
*fetchAndSaveJoke() {
const { dispatch } = yield getProps;
// Trigger a reducer to set a loading state in your store, which the UI can key off of
dispatch(actions.chuckRequest());
try {
const response = yield fetch('jokes/random');
dispatch(actions.chuckSuccess(response.response.value.joke));
} catch(err) {
dispatch(actions.chuckFailure(err));
}
},
};