React call logger action from reducers - reactjs

Trying to call an action from actions/reducers with no success. I know this is an anti-pattern, but I want to custom log (will have a log view) of what is happening in the app. A view will list these items.
When running this I get the error:
Reducers may not dispatch actions
If I understand it correctly I should use actors but can't find good examples.
Any suggestions on how make a custom logger that can be triggered from reducers and actions (works fine from Components)
actions/log.js
import {appError} from './index';
import moment from 'moment'
import {store} from '../containers/app';
// ------- LOG WHATS HAPPENING -------
export const LOG_ADD = 'LOG_ADD';
export function logAdd(item){
return {
type: LOG_ADD,
item: item
}
}
export function triggerLog(log){
return function (dispatch, getState) {
dispatch(logAdd(log));
}
}
export const LOG_ITEM_ERROR = "logerror";
export const LOG_ITEM_NOTIFY = "lognotify";
export class Log {
constructor(type,title,msg1,msg2) {
this.date = moment().unix();
this.type = type;
this.title = title;
this.msg1 = msg1;
this.msg2 = msg2;
}
static error(title,msg1,msg2){
dispatch( triggerLog( new Log(LOG_ITEM_ERROR,title,msg1,msg2) ) );
}
static notify(title,msg1,msg2){
store.dispatch( triggerLog( new Log(LOG_ITEM_NOTIFY,title,msg1,msg2) ) );
}
}
reducers/version.js
export default function version(state = initialVersion,action){
switch(action.type){
case VERSION_RESPONSE:
if(action.json.UpdateRequired){
console.log("FORCE UPDATE");
Log.error('Version','Force update');
//#TODO make sure app gets updated
return;
}
Log.notify('Version','Check complete');
return Object.assign({}, state, {
isDone: true,
isFetching: false,
isValid: true
})
SOLUTION
Upgraded to react-native 0.30
Do not use the dispatch in action:
export function getVersion() {
return function (dispatch, getState) {
dispatch(mycall()); // <-- do not do this
Now the code example works.

If you want to dispatch non-primitive actions you need redux-thunk
Redux has also have nice example which is using this thunkMiddleware.
If you only want to log state changes you can use Chrome Redux plugin on use this simple redux-logger middleware.
Or write your own custom middleware

Related

How do I use Redux Toolkit with multiple React App instances?

We have written a React app using Redux via Redux Toolkit. So far so fine.
Now the React app shall be rendered into multiple different elements (each element shall get a new app instance) on the same page.
The rendering part is straight forward: We just call ReactDOM.render(...) for each element.
The Redux part again brings some headache.
To create a new Redux store instance for each app instance, we call the configureStore function for each React app instance. Our slices look similiar to this:
import { createSlice } from '#reduxjs/toolkit'
import type { RootState } from '../../app/store'
// Define a type for the slice state
interface CounterState {
value: number
}
// Define the initial state using that type
const initialState: CounterState = {
value: 0,
}
const counterSlice = createSlice({
name: 'counter',
// `createSlice` will infer the state type from the `initialState` argument
initialState,
reducers: {
increment: (state) => {
state.value += 1
},
decrement: (state) => {
state.value -= 1
}
},
});
export const increment = (): AppThunk => async (
dispatch: AppDispatch
) => {
dispatch(indicatorsOrTopicsSlice.actions.increment());
};
export const decrement = (): AppThunk => async (
dispatch: AppDispatch
) => {
dispatch(indicatorsOrTopicsSlice.actions.decrement());
};
// Other code such as selectors can use the imported `RootState` type
export const selectCount = (state: RootState) => state.counter.value
export default counterSlice.reducer
Please note, that currently we create and export each slice statically and only once. Here comes my first question: Is this actually valid when creating multiple store instances or do we actually need to create also new slice instances for each app/store instance?
For the simple counter example provided, doing not so, seems to work, but as soon as we use an AsyncThunk as in the example below the whole thing breaks.
import { createAsyncThunk, createSlice } from '#reduxjs/toolkit'
import { userAPI } from './userAPI'
// First, create the thunk
const fetchUserById = createAsyncThunk(
'users/fetchByIdStatus',
async (userId, thunkAPI) => {
const response = await userAPI.fetchById(userId)
return response.data
}
)
// Then, handle actions in your reducers:
const usersSlice = createSlice({
name: 'users',
initialState: { entities: [], isLoading: false, hasErrors: false },
reducers: {
// standard reducer logic, with auto-generated action types per reducer
},
extraReducers: (builder) => {
builder.addCase(fetchUserById.pending, (state, action) => {
state.isLoading = true;
});
builder.addCase(fetchUserById.rejected, (state, action) => {
state.isLoading = false;
state.hasErrors = true;
});
builder.addCase(fetchUserById.fulfilled, (state, action) => {
// Add user to the state array
state.entities.push(action.payload);
state.isLoading = false;
state.hasErrors = true;
});
},
});
I believe the breaking starts here because of interdifferences between the events fired from dispatching the AsyncThunk.
Thereby I think the solution is to call the createAsyncThunk function for each app/store/slice instance. Are there any best practices for doing so? Of course this breaks the beauty and functionality of static exports and requires kind of a mapping, hence I'm asking.
My original suspicion that the AsyncThunk-part was responsible for the interferences between the stores of the different React app instances was wrong.
The source was something different not visible in the examples provided in my question.
We use memoized selectors via createSelector from reselect. Those were created and exported like the rest statically which in fact is a problem when working with multiple store/app instances. This way all instances use the same memoized selector which again doesn't work correctly thereby, since in the worst scenario the stored values of the dependency selectors are coming from the use from another store/app instance. This again can lead to endless rerenderings and recomputations.
The solution I came up with, is to create the memoized selectors for each app instance freshly. Therefore I generate a unique id for each app instance which is stored permanently in the related Redux store. When creating the store for an app instance I create also new memoized selectors instances and store them in a object which is stored in a static dictionary using the appId as the key.
To use the memoized selectors in our components I wrote a hook which uses React.memo:
import { useMemo } from "react";
import { useSelector } from "react-redux";
import { selectAppId } from "../redux/appIdSlice";
import { getMemoizedSelectors } from "../redux/memoizedSelectors";
// Hook for using created memoized selectors
export const useMemoizedSelectors = () => {
const appId = useSelector(selectAppId);
const allMemoizedSelectors = useMemo(() => {
return getMemoizedSelectors(appId);
}, [appId]);
return allMemoizedSelectors;
};
Then the selectors can be used in the components like this:
function MyComponent(): ReactElement {
const {
selectOpenTodos,
} = useMemoizedSelectors().todos;
const openTodos = useSelector(selectOpenTodos);
// ...
}
and the related dictionary and lookup process would look like this:
import { createTodosMemoizedSelectors } from "./todosSlice";
/**
* We must create and store memoized selectors for each app instance on its own,
* else they will not work correctly, because memoized value would be used for all instances.
* This dictionary holds for each appId (the key) the related created memoized selectors.
*/
const memoizedSelectors: {
[key: string]: ReturnType<typeof createMemoizedSelectors>;
} = {};
/**
* Calls createMemoizedSelectors for all slices providing
* memoizedSelectors and stores resulting selectors
* structured by slice-name in an object.
* #returns object with freshly created memoized selectors of all slices (providing such selectors)
*/
const createMemoizedSelectors = () => ({
todos: createTodosMemoizedSelectors(),
});
/**
* Creates fresh memoized selectors for given appId.
* #param appId the id of the app the memoized selectors shall be created for
*/
export const initMemoizedSelectors = (appId: string) => {
if (memoizedSelectors[appId]) {
console.warn(
`Created already memoized selectors for given appId: ${appId}`
);
return;
}
memoizedSelectors[appId] = createMemoizedSelectors();
};
/**
* Returns created memoized selectors for given appId.
*/
export const getMemoizedSelectors = (appId: string) => {
return memoizedSelectors[appId];
};

How middleware in react life cycle works?

I am new in react js. I have started doing a small product with react-redux. I am using saga middle-ware.
What i have done is as under.
This is the component
//all import work
import { activateAuthLayout, onLoad } from '../../../store/actions';
class EcommerceProductEdit extends Component {
constructor(props) {
super(props);
this.state = {
checked: false,
unselected_lists: [],
main_checked: false
}
//here I get the products props always null
console.log(this.props);
}
componentDidMount() {
this.props.activateAuthLayout();
//dispatching an action to fetch data from api, done in midddleware
if (this.props.user !== null && this.props.user.shop_id)
this.props.onLoad({
payload: this.props.user
});
}
render() {
//here I get the products props
console.log(this.props);
return (
//jsx work
);
}
}
const mapStatetoProps = state => {
const { user, is_logged_in } = state.Common;
const { products, is_loading } = state.Products;
return { user, is_logged_in, products, is_loading };
}
export default withRouter(connect(mapStatetoProps, { activateAuthLayout, onLoad })(EcommerceProductEdit));
Action is
import { FETCH_PRODUCT, FETCH_PRODUCT_SUCCESS } from './actionTypes';
export const onLoad = (action) => {
return {
type: FETCH_PRODUCT,
payload: action.payload
}
}
export const productFetched = (action) => {
return {
type: FETCH_PRODUCT_SUCCESS,
payload: action.payload
}
}
Reducer is
import { FETCH_PRODUCT_SUCCESS } from './actionTypes';
const initialState = {
products: null,
is_loading: true
}
export default (state = initialState, action) => {
switch (action.type) {
case FETCH_PRODUCT_SUCCESS:
state = {
...state,
products: action.payload,
is_loading: false
}
break;
default:
state = { ...state };
break;
}
return state;
}
And saga is
import { takeEvery, put, call } from 'redux-saga/effects';
import { FETCH_PRODUCT } from './actionTypes';
import { productFetched } from './actions';
import agent from '../../agent';
function* fetchProduct(action) {
try {
let response = yield call(agent.Products.get, action.payload);
yield put(productFetched({ payload: response }));
} catch (error) {
if (error.message) {
console.log(error);
} else if (error.response.text === 'Unauthorized') {
console.log(error)
}
}
}
function* productSaga() {
yield takeEvery(FETCH_PRODUCT, fetchProduct)
}
export default productSaga;
I am being able to get the products props only in render function. How would i be able to get it it in constructor ?
I would be really grateful if anyone explained me about react life cycle a little bit more.
Thanks.
updated
a constructor is called during object instantiation. According to the docs "The constructor for a React component is called before it is mounted". So if the props passed to the component are being changed after the component has been mounted you can use componentWillReceiveProps life cycle methods.
componentWillReceiveProps is deprecated so you can use componentDidUpdate instead. Example from the docs.
componentDidUpdate(prevProps) {
// Typical usage (don't forget to compare props):
if (this.props.userID !== prevProps.userID) {
// update your component state from here.
this.fetchData(this.props.userID);
}
}
MiddleWare: Middleware just comes in between the flow after the action has been dispatched and before it reaches the reducers, like in your case once you fire onLoad action and before it reaches the reducers, its caught in Saga middleware which executes it according to code written in it
Lifecycle in your case goes the following way:
In your compoenentDidMount method, you dispatch an action of onLoad. The action type in such a case becomes "FETCH_PRODUCT" and same action is now caught in Saga.
Since this is async call, the code in your component continues executing while the Saga perform its action in parallel. It calls API through this line of code: yield call(agent.Products.get, action.payload); . Once API call is completed, it dispatches an action 'productfetched' through this line of code yield put(productFetched({ payload: response }));.
Now this action reaches reducer and modify the state of "products". Since the product state in your redux is modified, your component EcommerceProductEdit re-renders and you get your product list in render method. The point to be noted is that the flow must have already finished executing inside componentDidMount method by this time, so no chance of having products their
Solution to your problem:
Once an action is dispatched and which has become async due to Saga, you won't be able to get value in constructor, if you use Saga. You can just directly call upon the API using axios/fetch library in componentDidMount and await their (Making it synchronous). Once you get response, you may proceed further
In case you have functional component, then you may use Effect hook and bind the dependency to products state. You can write your code in this block, what you want to be executed after API call is made and product list modifies.
React.useEffect(
() => {
// You code goes here
},
[products]
);
You just have to console props rather than doing this.props. You should not reference props with this inside the constructor.
Do this instead:
console.log(props)
Middleware is not related to react lifecycle at all, other than it updates and connected components "react" to props updating.
Check the constructor docs
https://reactjs.org/docs/react-component.html#constructor
Question: why are you trying to log props in the constructor anyway? If you want to know what the props are, use one of the lifecycle functions, componentDidMount/componentDidUpdate, don't use the render function to do side-effects like make asynchronous calls or console log.
componentDidMount() {
console.log(this.props);
}
If you must log props in the constructor though, access the props object that was passed as the component won't have a this.props populated yet.
constructor(props) {
super(props);
...
console.log(props);
}

How to fetch updated state in Component while using Redux

I am new to redux, I am getting confused about how to get State from Redux Store when we have multiple Reducer.
let Say this is my combineReducer
import apiCallInProgress from './ApiStattusReducer';
import login from './loginReducer';
const rootReducer = combineReducers({
login,
apiCallInProgress
});
export default rootReducer;
// below is my Login Reducer
const initialState = {
isAuthenticated: false,
user: {}
};
export function loginReducer(state = initialState, action = {}) {
console.log(action.user);
switch (action.type) {
case types.SET_CURRENT_USER:
return {
...state,
isAuthenticated: true,
user: action.user
}
default:
return state
}
}
export default loginReducer;
// now I want to access user from my component,
// I wrote as
function mapStateToProps(state) {
return {
login: state.login
}
}
componentDidMount() {
const { login } = this.props
console.log("set user didmount:" + login.user)
}
componentDidUpdate() {
const { login } = this.props
console.log("set user didupdate:" + login.user)
}
I am not able to get the state of user in the component but when I am pressing login button console.log(action.user) showing proper output in console .
The variable names we mentioned inside combineReducer, the same name do I need to use inside mapStateToProps func to fetch the state. I am very much confused. Someone, please explain.
I think you're doing everything right till you get to the component (although difficult to fully determine without actually testing the code). The main issue is, as far as I can see, you're not actually dispatching the action so login.user is never being set.
The connect method of react-redux has two function parameters - mapStateToProps, which you're using correctly, and mapDispatchToProps, which it doesn't look like you're using.
mapDispatchToProps is a method to pass Redux actions into props, which when invoked, will fire the action, which will in turn be picked up by the reducer, which will return a new state object. Create a new directory called actions in the root of your app. Inside it, create a file called loginActions.js, and within that, put something like the following:
export function setCurrentUser(user) {
return (dispatch) => {
dispatch({ type: 'SET_CURRENT_USER', user)
}
}
import this function into your component, then in your connect function, add mapDispatchToProps
import { connect } from 'react-redux'
import { setCurrentUser } from './actions/loginActions'
// component code
const mapDispatchToProps = {
setCurrentUser: setCurrentUser
}
const mapStateToProps = (state) => {
return {
login: state.login
}
}
connect(mapStateToProps, mapDispatchToProps)(YourComponentName)
In your componentDidMount method, you can now call this action:
componentDidMount() {
this.props.setCurrentUser('myUser')
}
in componentDidUpdate, the user should now be available:
componentDidUpdate(prevProps) {
if (prevProps.login !== this.props.login) {
console.log(this.props.login)
}
}
I hope this helps. Let me know if you need any more help.

Wait for redux action to finish dispatching when using redux saga

I have a redux saga setup which works fine. One of my dispatches is to create a new order, then once that has been created I want to do things with the updated state.
// this.props.userOrders = []
dispatch(actions.createOrder(object))
doSomethingWith(this.props.userOrders)
Since the createOrder action triggers a redux saga which calls an API, there is a delay, so this.props.userOrders is not updated before my function doSomethingWith is called. I could set a timeout, but that doesn't seem like a sustainable idea.
I have read the similar questions on Stack Overflow, and have tried implementing the methods where relevant, but I can't seem to get it working. I'm hoping with my code below that someone can just add a couple of lines which will do it.
Here are the relevant other files:
actions.js
export const createUserOrder = (data) => ({
type: 'CREATE_USER_ORDER',
data
})
Sagas.js
function * createUserOrder () {
yield takeEvery('CREATE_USER_ORDER', callCreateUserOrder)
}
export function * callCreateUserOrder (newUserOrderAction) {
try {
const data = newUserOrderAction.data
const newUserOrder = yield call(api.createUserOrder, data)
yield put({type: 'CREATE_USER_ORDER_SUCCEEDED', newUserOrder: newUserOrder})
} catch (error) {
yield put({type: 'CREATE_USER_ORDER_FAILED', error})
}
}
Api.js
export const createUserOrder = (data) => new Promise((resolve, reject) => {
api.post('/userOrders/', data, {headers: {'Content-Type': 'application/json'}})
.then((response) => {
if (!response.ok) {
reject(response)
} else {
resolve(data)
}
})
})
orders reducer:
case 'CREATE_USER_ORDER_SUCCEEDED':
if (action.newUserOrder) {
let newArray = state.slice()
newArray.push(action.newUserOrder)
return newArray
} else {
return state
}
This feels like an XY Problem. You shouldn't be "waiting" inside a component's lifecycle function / event handler at any point, but rather make use of the current state of the store.
If I understand correctly, this is your current flow:
You dispatch an action CREATE_USER_ORDER in your React component. This action is consumed by your callCreateUserOrder saga. When your create order saga is complete, it dispatches another "completed" action, which you already have as CREATE_USER_ORDER_SUCCEEDED.
What you should now add is the proper reducer/selector to handle your CREATE_USER_ORDER_SUCCEEDED:
This CREATE_USER_ORDER_SUCCEEDED action should be handled by your reducer to create a new state where some "orders" property in your state is populated. This can be connected directly to your component via a selector, at which point your component will be re-rendered and this.props.userOrders is populated.
Example:
component
class OrderList extends React.PureComponent {
static propTypes = {
userOrders: PropTypes.array.isRequired,
createOrder: PropTypes.func.isRequired,
}
addOrder() {
this.props.createOrder({...})
}
render() {
return (
<Wrapper>
<Button onClick={this.addOrder}>Add Order</Button>
<List>{this.props.userOrders.map(order => <Item>{order.name}</Item>)}</List>
</Wrapper>
)
}
}
const mapStateToProps = state => ({
userOrders: state.get('userOrders'),
})
const mapDispatchToProps = {
createOrder: () => ({ type: 'CREATE_ORDER', payload: {} }),
}
export default connect(mapStateToProps, mapDispatchToProps)(OrderList)
reducer
case 'CREATE_USER_ORDER_SUCCEEDED':
return state.update('userOrders',
orders => orders.concat([payload.newUserOrder])
)
If you really do need side-effects, then add those side-effects to your saga, or create a new saga that takes the SUCCESS action.

Correct way to pre-load component data in react+redux

I do not know the correct way to pre-load data from API for a component to use.
I have written a stateless component which should render the data:
import React, { PropTypes } from 'react';
const DepartmentsList = ({ departments }) => {
const listItems = departments.map((department) => (
<li>{department.title}</li>
));
return (
<ul>
{listItems}
</ul>
);
};
DepartmentsList.propTypes = {
departments: PropTypes.array.isRequired
};
export default DepartmentsList;
And I have an action which will retreive data from the API:
import { getDepartments } from '../api/timetable';
export const REQUEST_DEPARTMENTS = 'REQUEST_DEPARTMENTS';
export const RECEIVE_DEPARTMENTS = 'RECEIVE_DEPARTMENTS';
const requestDepartments = () => ({ type: REQUEST_DEPARTMENTS });
const receiveDepartments = (departments) => ({ type: RECEIVE_DEPARTMENTS, departments });
export function fetchDepartments() {
return dispatch => {
dispatch(requestDepartments);
getDepartments()
.then(departments => dispatch(
receiveDepartments(departments)
))
.catch(console.log);
};
}
Now I think I have a few options to preload departments that are required for the list. I could use redux-thunk and mapDispatchToProps to inject fetchDepartments to the stateless component and implement componentWillMount or similar lifecycle method, to load data - but then I don't need to pass the list via props, as the component would always load data for himself, and I don't want that, because whenever a new component is created the data is fetched from api instead of store...
Another advice I've seen is to use getComponent function from react-router, and retreive all data before returning the component, however, I am not sure if it's the correct redux way, as I don't see how to use redux-thunk there, and logic kind of seems littered all accross the files, when it's the data required for only one component.
This leaves me with the only seemingly ok option to load data in container component's lifecycle methods, but I want to know what is considered the best practice for what I want to do.
The most 'redux-like' way of handling the pre-loading of data would be to fire off the asynchronous action in the lifecycle method (probably componentWillMount) of a Higher Order Component that wraps your app. However, you will not use the results of the API call directly in that component - it needs to be handled with a reducer that puts it into your app store. This will require you to use some sort of a thunk middleware to handle the asynchronous action. Then you will use mapStateToProps to simply pass it down to the component that renders the data.
Higher Order Component:
const mapStateToProps = (state) => {
return {
departments: state.departments
};
}
const mapDispatchToProps = (dispatch) => {
return bindActionCreators({
getDepartments: actionCreators.fetchDepartments
});
}
class App extends Component {
componentWillMount() {
this.props.getDepartments();
}
render() {
return <DepartmentsList departments={this.props.departments} />
}
}
export default connect(mapStateToProps, mapDispatchToProps)(App);
reducers:
export function departments(state = [], action) {
switch(action.type) {
case 'RECEIVE_DEPARTMENTS':
return action.departments;
}
}

Resources