Getting error when setting the whole Ngrx store state to undefined using `Meta Reducers` by help of dispatching logout action - javascript

I want to logout from my app and before logging out I want to make sure I have completely cleared my app ngrx store states using MetaReducer with the help of this code
import { StoreModule, ActionReducer, MetaReducer } from '#ngrx/store';
export function logout(reducer: ActionReducer<any>): ActionReducer<any> {
return function (state, action) {
if (action.type === 'LOGOUT') {
state = undefined;
}
return reducer(state, action);
};
}
export const metaReducers: MetaReducer<any>[] = [logout];
when I am inside of a component which is subscribed to one of ngrx store state for example list of invoices, so when I am clicking log out button from here so getting this error in the console
and the line which gives me the error is
this.$invoice.takeWhile(() => this.isComponentAlive).subscribe(
invoice => {
this.permission = this.departmentService.getPermissionOfInvoiceCurrentDepartment(invoice.currentDepartmentId);
}
);
if I am changing the code to this
this.$invoice.takeWhile(() => this.isComponentAlive).subscribe(
invoice => {
if (invoice) {
this.permission = this.departmentService.getPermissionOfInvoiceCurrentDepartment(invoice.currentDepartmentId);
}
}
);
it solves the problem but I have many places like this so I want to one-stop-shop solution for this anyone could help me with this.

Related

React hook, wired issue when use useState, while if use setState work perfectly, how to solve it

dear community, I am facing a wired issue, and I don't know how to summary my situation in the question title, so I wonder if the question title is accurate enough.
I was trying to convert a class component to a hook component.
The class version code like this
async componentDidMount() {
const { dispatch, itemId } = this.props;
try {
if (itemId) {
await dispatch({
type: 'assignment/fetchSubmissionsByAssignment', //here to fetch submissions in props
payload: {
id: itemId
}
});
}
const { submissions } = this.props;
this.setState({
studentSubmissions: submissions,
});
} catch (error) {
throw error.message;
}
}
render() {
const { studentSubmissions } = this.state;
return (
<Table dataSource={studentSubmissions} />
)
}
export default SubmissionsDetail;
and in hook, it look like this
const [studentSubmissions, setStudentSubmissions] = useState([]);
useEffect(() => {
async function fetchSubmissions() {
const { dispatch, itemId } = props;
try {
if (itemId) {
await dispatch({
type: 'assignment/fetchSubmissionsByAssignment',
payload: {
id: itemId
}
});
}
const { submissions } = props;
setStudentSubmissions(submissions)
} catch (error) {
throw error.message;
}
};
fetchSubmissions()
}, []);
return (
<Table dataSource={studentSubmissions} />
)
export default SubmissionsDetail;
I omitted some code for better reading, like connect to redux store or others.
and the component is import in the parent file like this
import SubmissionsDetail from './SubmissionsDetail'
{assignmentIds.map((itemId) => {
<SubmissionsDetail itemId={itemId}/>
})}
it work perfect in class component, the expected result should return tables like this
However, when I change to use hook, the result return like this
or sometimes all data in tables become submissions3
I try to console.log(submissions) inside the try{...} block, when in class, the result is
which is correct, there have two assignments, the one have 4 submissions, another one have zero submission.
But the output in hook is different, the result is like this
either both have 4 submissions, either both have zero. That means one obj affect all other obj.
It seems like if useState change, it would influence other objs, that make me really confused. I think in the map method, each item is independent, right? If so, and how to explain why it work perfectly in class setState, but failed in hook useState?
I hope my question is clear enough, If you know how to describe my question in short, plz let me know, I would update the title, to help locate experts to answer.
Please don't hesitate to share your opinions, I really appreciate and need your help, many thanks!
Edit: You are probably going to want to rework the way you store the submission inside of the redux store if you really want to use the Hook Component. It seems like right now, submissions is just an array that gets overwritten whenever a new API call is made, and for some reason, the Class Component doesn't update (and it's suppose to update).
Sorry it's hard to make suggestions, your setup looks very different than the Redux environments I used. But here's how I would store the submissions:
// no submissions loaded
submissions: {}
// loading new submission into a state
state: {
...state,
sessions: {
...state.session,
[itemId]: data
}
}
// Setting the state inside the component
setStudentSubmissions(props.submissions[itemId])
And I think you will want to change
yield put({
type: 'getSubmissions',
payload: response.data.collections
});
to something like
yield put({
type: 'getSubmissions',
payload: {
data: response.data.collections,
itemId: id
});
If you want to try a "hack" you can maybe get a useMemo to avoid updating? But again, you're doing something React is not suppose to do and this might not work:
// remove the useEffect and useState, and import useMemo
const studentSubmissions = useMemo(async () => {
try {
if (itemId) {
await dispatch({
type: "assignment/fetchSubmissionsByAssignment", //here to fetch submissions in props
payload: {
id: itemId,
},
});
return this.props.submissions;
}
return this.props.submissions;
} catch (error) {
throw error.message;
}
}, []);
return (
<Table dataSource={studentSubmissions} />
)
export default SubmissionsDetail;
There is no reason to use a local component state in either the class or the function component versions. All that the local state is doing is copying the value of this.props.submissions which came from Redux. There's a whole section in the React docs about why copying props to state is bad. To summarize, it's bad because you get stale, outdated values.
Ironically, those stale values were allowing it to "work" before by covering up problems in your reducer. Your reducer is resetting the value of state.submissions every time you change the itemId, but your components are holding on to an old value (which I suspect is actually the value for the previous component? componentDidMount will not reflect a change in props).
You want your components to select a current value from Redux based on their itemId, so your reducer needs to store the submissions for every itemId separately. #Michael Hoobler's answer is correct in how to do this.
There's no problem if you want to keep using redux-saga and keep using connect but I wanted to give you a complete code so I am doing it my way which is with redux-toolkit, thunks, and react-redux hooks. The component code becomes very simple.
Component:
import React, { useEffect } from "react";
import { fetchSubmissionsByAssignment } from "../store/slice";
import { useSelector, useDispatch } from "../store";
const SubmissionsDetail = ({ itemId }) => {
const dispatch = useDispatch();
const submissions = useSelector(
(state) => state.assignment.submissionsByItem[itemId]
);
useEffect(() => {
dispatch(fetchSubmissionsByAssignment(itemId));
}, [dispatch, itemId]);
return submissions === undefined ? (
<div>Loading</div>
) : (
<div>
<div>Assignment {itemId}</div>
<div>Submissions {submissions.length}</div>
</div>
);
};
export default SubmissionsDetail;
Actions / Reducer:
import { createAsyncThunk, createReducer } from "#reduxjs/toolkit";
export const fetchSubmissionsByAssignment = createAsyncThunk(
"assignment/fetchSubmissionsByAssignment",
async (id) => {
const response = await getSubmissionsByAssignment(id);
// can you handle this in getSubmissionsByAssignment instead?
if (response.status !== 200) {
throw new Error("invalid response");
}
return {
itemId: id,
submissions: response.data.collections
};
}
);
const initialState = {
submissionsByItem: {}
};
export default createReducer(initialState, (builder) =>
builder.addCase(fetchSubmissionsByAssignment.fulfilled, (state, action) => {
const { itemId, submissions } = action.payload;
state.submissionsByItem[itemId] = submissions;
})
// could also respond to pending and rejected actions
);
if you have an object as state, and want to merge a key to the previous state - do it like this
const [myState, setMyState] = useState({key1: 'a', key2: 'b'});
setMyState(prev => {...prev, key2: 'c'});
the setter of the state hook accepts a callback that must return new state, and this callback recieves the previous state as a parameter.
Since you did not include large part of the codes, and I assume everything works in class component (including your actions and reducers). I'm just making a guess that it may be due to the omission of key.
{assignmentIds.map((itemId) => {
<SubmissionsDetail itemId={itemId} key={itemId} />
})}
OR it can be due to the other parts of our codes which were omitted.

How to fix mapStateToProps from accessing an undefined state key without checking manually?

I am creating a single page web application with React, with the state stored using Redux. When I access something like state.workspace.guest.data in mapStateToProps() it causes an exception crashing the application.
It appears mapStateToProps() is invoked before componentDidMount(), which invokes this.props.getGuests() action that actually causes state.workspace.guest to be initialized. The page loads properly when I manually enter the URL and press enter. However, mapStateToProps() causes an exception when I navigate through the application, with state.workspace.guest being undefined.
A possible solution would be to check if state.workspace.guest is defined. However, I feel there is a better way to do it?
The answers in the following link suggest to use selectors. I am not sure if this is the solution to my problem, given I am new to Redux.
react js mapStateToProps triggers Uncaught TypeError: Cannot read property 'map' of undefined
Here are the mapping functions.
function mapDispatchToProps(dispatch) {
return bindActionCreators({
getGuests: Actions.getGuests
}, dispatch);
}
function mapStateToProps(state, ownProps) {
return {
guests : state.workspace.guest.data
}
}
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(GuestSearchTable));
The componentDidMount() function which actually triggers an action which in turn loads the data.
componentDidMount() {
this.props.getGuests();
}
Here is the reducer.
const initialState = {
data : []
};
function guestReducer(state = initialState, action) {
switch (action.type) {
case Actions.GET_GUESTS: {
return {
...state,
data : action.payload.data
};
}
case Actions.GET_GUEST: {
return {
...state,
guest: action.payload.data
};
}
case Actions.SAVE_GUEST: {
return {
...state,
guest: action.payload.data
};
}
default: {
return state;
}
}
};
I expect the initial value of the state.workspace.guest to be { data : [] }. But the actual value is undefined. The same problem appears in all the other pages.
You are trying to access data from guest key.
function mapStateToProps(state, ownProps) {
return {
guests : state.workspace.guest.data
}
}
I think you want to access state.workspace.data or you have to change key name to guests in reducer and the intialize state as follow:
return {
guests: state.workspace.guests
}

Redux state not updating with javascript object

I have this container and component and the object yeast that Im trying to put in my store. However, its not working whenever I try and save it. The object looks like this
{ yeastType : value, temp: value}
Container.js
const mapDispatchToProps = (dispatch) => {
return {
handleYeastChange: (yeast) => {
dispatch(actions.updateYeast(yeast))
}
}
};
const RecipeYeastContainer = connect(
mapStateToProps,
mapDispatchToProps
)(RecipeYeast);
Component.js
updateYeastState = (updatedYeast) => {
this.props.handleYeastChange(updatedYeast)
};
I have no errors in the console or anything. When I open my redux dev tools its telling me the state has already been updated by the time the action is called. And thus only ever saving the first letter thats input into my field. It never persists it. Its really weird. Its also never showing up in the UI. I can type as much as I want and see the action firing and the state keeping the first letter but its not showing up in the input.
Whats weird is that when I change the code to pass in both yeastType and temp to the property function and construct the object in there it works. (See below)
This works: Container.js
const mapDispatchToProps = (dispatch) => {
return {
handleYeastChange: (yeastType, temp) => {
const yeast = {yeastType, temp}
dispatch(actions.updateYeast(yeast))
}
}
};
Component.js
updateYeastState = (updatedYeast) => {
this.props.handleYeastChange(updatedYeast.yeastType, updatedYeast.temp)
};
I cannot figure out why this is happening. I thought I could just pass the object all the way through and not have to reconstruct it.
Do you dispatch your action correctly? And in using redux, you are not updating the state of the component, you're updating the store and then the value in component is from your mapStateToProps function that get from the store. Say it you're updating your store with the object named yourReducer store. ex:
Container.js:
const mapStateToProps = (state) => ({
inputValue: state.yourReducer.value
})
const mapDispatchToProps = (dispatch) => ({
inputHandler: (e) => {
dispatch(yourAction({type: 'CHANGE_INPUT', value: e.target.value}))
// The store catches the action and pass to the reducer that can read the action's type
// in `yourReducer` will catch the 'CHANGE_INPUT' type and
// return a new value as same as the value that passed from the action
}
})
export default connect(mapStateToProps)(Component)
Component.js
export default class Component extends React.Component {
{ your custom function here ... }
render() {
const { inputValue, inputHandler } = this.props
return (
<div>
{/* The input will be the same as the inputValue changed */}
<input value={inputValue} onChange={inputHandler} />
</div>
)
}
}
For debugging redux you can try this redux-devtool.

Good pattern for redux action in callback

Supposing I have an update comment action. When a user updates comment after getting a successful result from Promise I should close comment editor. This is my sample code from my project:
export const updateComment = (comment,callBack/* ? */) => {
return (dispatch, getState){
api.updateComment({...comment}).then((result) => {
/* Do something */
callback() /* ? */
})
}
}
In react component I use action like the following code:
handleUpdateComment(){
dispatch(actions.updateComment(this.state.comment,this.closeCommentEitor)
}
It works well but I think is not a good pattern to close comment editor. I'm looking a correct pattern to close editor without passing callBack like I did if any.
When you are using redux-thunk, you can dispatch an action from another action.
What you can do is that, commentEditor have a state which you store in redux and based on that state open or close the commentEditor
export const updateComment = (comment, comment_id) => {
return (dispatch, getState){
api.updateComment({...comment}).then((result) => {
/* Do something */
dispatch({type: 'CLOSE_COMMENT_EDITOR', id: comment_id})
})
}
}
After this in a reducer on this action change the state of redux store, something like
import update from 'immutability-helper'
var initialState = [{commentId: '1', commentEditorOpenStatus: false}, {commentId: '2', commentEditorOpenStatus: false}]
const reducer = (state = initialState, action) => {
switch(action.type) {
'CLOSE_COMMENT_EDITOR':
const idx = state.findIndex(obj => obj.commentId == action.id);
return update(state, {
[idx]: {
commentEditorOpenStatus: {
$set: false
}
}
})
// Other action handlers here
default: return state
}
}
The only thing that updates your application's state is your reducers.
The reducer should be responsible to update the state of your application and not your action (you are now passing getState).
I suggest you to look at redux-promise-middleware
The middleware enables optimistic updates and dispatches pending, fulfilled and rejected actions, which can be intercepted by the reducer.

Add logic to the store?

I have a redux application with a "campaign" reducer/store.
Currently I have repeated code to check if a specific campaign is loaded or needs an API call to fetch details from the DB. Much simplified it looks like this:
// Reducer ----------
export default campaignReducer => (state, action) {
const campaignList = action.payload
return {
items: {... campaignList}
}
}
// Component ----------
const mapStateToProps = (state, ownProps) => {
const campaignId = ownProps.params.campaignId;
const campaign = state.campaign.items[campaignId] || {};
return {
needFetch: campaign.id
&& campaign.meta
&& (campaign.meta.loaded || campaign.meta.loading),
campaign,
};
}
export default connect(mapStateToProps)(TheComponent);
Now I don't like to repeat the complex condition for needFetch. I also don't like to have this complex code in the mapStateToProps function at all, I want to have a simple check. So I came up with this solution:
// Reducer NEW ----------
const needFetch = (items) => (id) => { // <-- Added this function.
if (!items[id]) return true;
if (!items[id].meta) return true;
if (!items[id].meta.loaded && !items[id].meta.loading) return true;
return false;
}
export default campaignReducer => (state, action) {
const campaignList = action.payload
return {
needFetch: needFetch(campaignList), // <-- Added public access to the new function.
items: {... campaignList}
}
}
// Component NEW ----------
const mapStateToProps = (state, ownProps) => {
const campaignId = ownProps.params.campaignId;
const campaign = state.campaign.items[campaignId] || {};
return {
needFetch: state.campaign.needFetch(campaignId), // <-- Much simpler!
campaign,
};
}
export default connect(mapStateToProps)(TheComponent);
Question: Is this a good solution, or does the redux-structure expect a different pattern to solve this?
Question 2: Should we add getter methods to the store, like store.campaign.getItem(myId) to add sanitation (make sure myId exists and is loaded, ..) or is there a different approach for this in redux?
Usually computational components should be responsible for doing this type of logic. Sure your function has a complex conditional check, it belongs exactly inside your computational component (just like the way you currently have it).
Also, redux is only for maintaining state. There's no reason to add methods to query values of the current state inside your reducers. A better way would be having a module specifically for parsing your state. You can then pass state to the module and it would extract the relevant info. Keep your redux/store code focused on computing a state only.
Your approach is somewhat against the idiomatic understanding of state in redux. You should keep only serializable data in the state, not functions. Otherwise you loose many of the benefits of redux, e.g. that you can very easily stash your application's state into the local storage or hydrate it from the server to resume previous sessions.
Instead, I would extract the condition into a separate library file and import it into the container component where necessary:
// needsFetch.js
export default function needsFetch(campaign) {
return campaign.id
&& campaign.meta
&& (campaign.meta.loaded || campaign.meta.loading);
}
// Component ----------
import needsFetch from './needsFetch';
const mapStateToProps = (state, ownProps) => {
const campaignId = ownProps.params.campaignId;
const campaign = state.campaign.items[campaignId] || {};
return {
needFetch: needsFetch(campaign),
campaign,
};
}
export default connect(mapStateToProps)(TheComponent);

Categories

Resources