I have two problems when trying to update my state.
First of all, the first letter in the input is not being updated directly to the state - in the console.log I can see that the useReducer is first calling the initialState and then is dispatching the actions, so the registered input is one letter behind the actual user's input.
Could you please guide me and show me what would be the best way to update the state of my object? I think I should divide the dispatch on more keys, but I feel a little bit lost and don't really know how to dispatch it correctly.
PS of course I dispatched more actions - that work - so I hid some of them, hence the structure of initial state :)
Calling useReducer in Inputs.ts
const [store, dispatch] = useReducer(reducer, initialState);
Actions.ts
export const SET_INPUT_STATE = 'SET_INPUT_STATE';
export const setInputState = (dispatch, payload) => dispatch({ type: SET_INPUT_STATE, payload });
Store.ts
import { SET_INPUT_STATE } from './actions';
export interface StateType {
formState: Record<string, { value: string; isValid: boolean }>;
}
export const initialState: StateType = {
inputState: {
email: {
value: '',
isValid: false,
},
password: {
value: '',
isValid: false,
},
confirmedPassword: {
value: '',
isValid: false,
},
name: {
value: '',
isValid: false,
},
},
};
export const reducer = (state: StateType, action): StateType => {
const { type, payload } = action;
switch (type) {
case SET_FORM_STATE:
return {
...state,
inputState: { ...state.inputState, [payload.name]: { isValid: payload.isValid, value: payload.value } },
};
default:
return state;
}
};
Related
I'm learning TypeScript and I have a difficulty to setup my reducer properly.
BasketContext.tsx
import React, { useContext, useReducer } from "react";
import BasketReducer from "../reducers/BasketReducer";
const BasketContext = React.createContext<any>(undefined);
export interface IBasketState {
products: [
{
name: string;
price: number;
quantity: number;
}
];
totalPrice: number;
}
const initialState: IBasketState = {
products: [
{
name: "",
price: 0,
quantity: 0,
},
],
totalPrice: 0,
};
export const BasketContextProvider = ({ children }: { children: ReactNode }) => {
const [basketState, dispatch] = useReducer<IBasketState>(BasketReducer, initialState);
return <BasketContext.Provider value={{ basketState }}>{children}</BasketContext.Provider>;
};
export const useBasketContext = () => {
return useContext(BasketContext);
};
BasketReducer.ts
import React from "react";
import { IBasketState } from "../contexts/BasketContext";
interface IBasketAction {
type: "Add item" | "Remove item";
payload?: string;
}
const BasketReducer = (basketState: IBasketState, action: IBasketAction) => {
if (action.type === "Add item") {
console.log("Add iteeeeeeem");
return { ...basketState };
}
};
export default BasketReducer;
When I hover my mouse over const [basketState, dispatch] = useReducer<IBasketState>(BasketReducer, initialState); I've got an error:
Type 'IBasketState' does not satisfy the constraint 'Reducer<any, any>'.
Type 'IBasketState' provides no match for the signature '(prevState: any, action: any): any'.ts(2344)
Also is it possible to initialize my initialState with empty array products? Currently I use empty string and 0.
Your reducer function must return a state (either a new state or the unmodified, passed-in state) in all cases. In your code, you are implicitly returning undefined if there's no match for the incoming action.
// add a return-type annotation
const BasketReducer =
(basketState: IBasketState, action: IBasketAction) : IBasketState => {
if (action.type === "Add item") {
console.log("Add iteeeeeeem");
return { ...basketState };
}
return basketState; // no match, return same state
};
You are also using incorrect generic types for your useReducer<IBasketState>(... call. The generic types for this call are a little complex, but you should be able to remove the generic type annotations and just let the compiler correctly infer the right types for your reducer i.e.:
useReducer(BasketReducer, initialState)
I have a react app that is connected with redux. The component has a form that makes a PUT call to the api when the form is submitted. When I submit the form, I can see that redux gets updated accordingly but when I try to access the redux state as a prop in my component, the props data does not return the current data and is off by 1. For example, here's the data in my redux store:
Redux store:
When I do console.log("THIS PROPS: ", this.props) in my component, I see that it accountError is showing up as null
When I dispatch the action again the second time, only then I see that I am getting the data from redux in my props:
Here is the code that I have currently:
OrgAccount.js
import { registerOrgAccount, getListOfOrgsAndAccts } from "../../store/actions";
handleSubmit = () => {
this.props.registerOrgAccount(this.state)
console.log("THIS PROPS: ", this.props)
if(this.props.accountError === null) {
this.toggleTab(this.state.activeTab + 1);
}
};
<Link
to="#"
className="btn w-lg"
onClick={() => {
if (this.state.activeTab === 1) {
this.handleSubmit();
}
}}
>
Next
</Link>
const mapStatetoProps = (state) => {
const { accounts, accountError, loading } = state.OrgAccount;
return { accounts, accountError, loading };
};
const mapDispatchToProps = (dispatch) => {
return {
getListOfOrgsAndAccts: () => {
dispatch(getListOfOrgsAndAccts())
},
registerOrgAccount: (data) => {
dispatch(registerOrgAccount(data))
},
}
}
export default connect(mapStatetoProps, mapDispatchToProps)(OrgAccount);
Reducer:
const initialState = {
accountError: null, accountsError: null, message: null, loading: null
}
const orgAccount = (state = initialState, action) => {
switch (action.type) {
case REGISTER_ORG_ACCOUNT:
state = {
...state,
account: null,
loading: true,
// accountError: null
}
break;
case REGISTER_ORG_ACCOUNT_SUCCESSFUL:
state = {
...state,
account: action.payload,
loading: false,
accountError: null
}
break;
case REGISTER_ORG_ACCOUNT_FAILED:
state = {
...state,
loading: false,
accountError: action.payload ? action.payload.response : null
}
break;
...
default:
state = { ...state };
break;
}
return state;
}
export default orgAccount;
Action
export const registerOrgAccount = (account) => {
return {
type: REGISTER_ORG_ACCOUNT,
payload: { account }
}
}
export const registerOrgAccountSuccessful = (account) => {
return {
type: REGISTER_ORG_ACCOUNT_SUCCESSFUL,
payload: account
}
}
export const registerOrgAccountFailed = (error) => {
return {
type: REGISTER_ORG_ACCOUNT_FAILED,
payload: error
}
}
Saga.js
import { registerOrgAccountSuccessful, registerOrgAccountFailed, getListOfOrgsAndAcctsSuccessful, getListOfOrgsAndAcctsFailed } from './actions';
import { putOrgAccount } from '../../../helpers/auth_helper';
function* registerOrgAccount({ payload: { account } }) {
try {
const response = yield call(putOrgAccount, {
orgId: account.orgId,
accountNumber: account.accountNumber,
accountName: account.accountName,
accountCode: account.accountCode,
urlLink: account.urlLink,
location: account.location,
accountType: account.accountType,
address: account.address,
city: account.city,
state: account.state,
zip: account.zip,
country: account.country,
email: account.email,
eula: "blah"
});
yield put(registerOrgAccountSuccessful(response));
} catch (error) {
yield put(registerOrgAccountFailed(error));
}
}
To understand the root cause here, I think it helps to know a little about immutability and how React rerenders. In short, React will rerender when it detects reference changes. This is why mutating a prop, wont trigger a rerender.
With that in mind, at the time you call handleSubmit, this.props.accountError is simply a reference to a value somewhere in memory. When you dispatch your action and your state is updated, a new reference will be created, which will trigger a rerender of your component. However the handleSubmit function that was passed to your element still references the old this.props.accountError, which is why it is still null.
You could get around this by implementing your check in the componentDidUpdate lifecycle method. E.g. something like this:
componentDidUpdate(prevProps) {
if (prevProps.accountError === null && this.props.accountError !== null) {
this.toggleTab(this.state.activeTab + 1)
}
}
I have the following reducer:
...
case PAGE_CHANGED: {
return {
...state,
property: {
...state.property,
currentPage: action.payload.page
}
}
}
...
my initialState looks like this:
export const initialState: RepairsState = {
property: {...s},
commercial: {...s}
}
where s is:
const s = {
data: [],
isFetching: false,
...paginationInitialState
}
and the paginationInitialState is like so:
const paginationInitialState: PaginationState = {
currentPage: 0,
orderBy: '',
resultsPerPage: 10,
searchTerm: '',
sortableFields: [],
totalCount: 0
}
commercial and property have the same state structure but will have different content.
When I update the PAGE_CHANGED reducer I use the following action:
export const pageChanged = (page: number): ThunkAction<void, AppState, null, Action<string>> => (
dispatch: Dispatch,
getState: () => AppState
) => {
const searchTerm = getLatestSearchTerm(getState())
dispatch({
type: PAGE_CHANGED,
payload: { page },
})
dispatch(searchForRepairs(searchTerm) as any)
}
for the commercial property I don't want recreate another action to update the commercial PAGE_CHANGED reducer. I want to avoid duplicating action logic.
I thought about passing an additional parameter when the action is called in the UI like so:
pageChanged(2, 'commercial')
so that I can have an if statement inside the action, is there a more clean solution than this?
If you're able dispatch pageChanged(2, 'commercial')
Then this might work:
export const commercialPageChanged = (page: number) => {
return pageChanged(page, 'commercial');
}
export const propertyPageChanged = (page: number) => {
return pageChanged(page, 'property');
}
That way your components are not dependent on knowing what value to pass as the second argument to pageChanged.
I use Redux in my project for first time. I have multiple reducers and and actions. When the first action is dispatched, state is changed. It looks okey. After dispatching second action, state is changed again but the previous changes are removed. I mean, when 'FETCH_COMPANY_INFORMATIONS' is dispatched companyName is changed and companyDesc set to initial value. Then 'FETCH_INITIAL_MEMBER' is dispatched and companyName is removed but companyDesc is still there and member payloads are also changed. What is my mistake? Thanks.
I tried many ways to solve this but still continue. I check this on Redux DevTools.
memberReducer
const initialState = {
username: '',
companyId: '',
isAdmin: '',
photo: '',
};
export default (state = initialState, action) => {
switch (action.type) {
case FETCH_INITIAL_MEMBER:
return {
...state,
username: action.payload.username,
companyId: action.payload.companyId,
isAdmin: action.payload.isAdmin,
};
default:
return state;
}
};
companyReducer
const initialState = {
companyName: 'companyName',
companyDesc: 'companyDesc',
};
export default (state = initialState, action) => {
switch (action.type) {
case FETCH_COMPANY_INFORMATIONS:
return {
...state,
companyName: action.payload.companyName,
};
default:
return state;
}
};
memberAction
const fetchInıtıalMember = async muuid => {
axios
.get(`/api/member/${muuid}`)
.then(response => {
const username = response.data.mname;
const isAdmin = response.data.misAdmin;
const companyId = response.data.cid;
store.dispatch({
type: FETCH_INITIAL_MEMBER,
payload: {
username,
isAdmin,
companyId,
},
});
})
.catch(error => {});
};
companyAction
const fetchCompanyInformations = () => {
store.dispatch({
type: FETCH_COMPANY_INFORMATIONS,
payload: { companyName: 'dispacthedCompanyName' },
});
};
Edit:
The code above is correct. My mistake is about importing the constants. This Redux implementation works well. I was storing all action type constant in a types.js file. I import this type constants in the another files wrongly. After changing it my problem is solved.
This is my state shape:
export default {
app: {
loading: false,
loadError: null
},
search: {
type: null,
text: ''
},
things: {
byId: {
},
allIds: []
}
}
When I fetch new things from the server, I want to update both things.byId and things.allIds - as recommended by Redux docs. I'm doing it in the following way but I feel like there's a better way:
import { LOAD_THINGS_SUCCESS } from '../actions/actionTypes'
import initialState from './initialState'
export default function things(state = initialState.things, action) {
switch(action.type) {
case LOAD_THINGS_SUCCESS:
const thingsById = {}
action.things.forEach(thing => {
thingsById[thing.id] = thing
})
return {
...state,
byId: thingsById,
allIds: action.things.map(thing => thing.id)
}
default:
return state
}
}
I'm using combineReducers and the above reducers/things.js is one of them. The part I'm concerned about is action.things.forEach... I have a feeling there's a cleaner / more efficient way of doing this.