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.
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)
Hello, I am new to redux and I am struggling with a problem. I am trying to access and map over the comments within my post array. However, I am not sure how to do this. So far, I've tried changing the actions and reducers in order to solve this issue. I think the problem is within the react and redux. I can't tell if my mapStateToProps is working correctly. Also, the state is being fetched from my express server and it seems to be working properly as you can see in the picture.
My getPost action:
export const getPost = (group_id, post_id) => async dispatch => {
try {
const res = await axios.get(`/api/groups/${group_id}/${post_id}`);
dispatch({
type: GET_POST,
payload: res.data
});
} catch (error) {
dispatch({
type: POST_ERROR,
payload: { msg: error.response.statusText, status: error.response.status }
});
}
};
The initial state:
const initialState = {
groups: [],
group: [],
loading: true,
error: {}
};
The reducer:
case GET_POST:
return {
...state,
post: payload,
loading: false
};
Where I'm trying to map over the comments:
import React, { Fragment, useEffect } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { getPost } from '../../../redux/actions/group';
const Post = ({ getPost, post, match }) => {
useEffect(() => {
getPost(match.params.group_id, match.params.post_id);
}, [getPost, match.params.group_id, match.params.post_id]);
// I want to map over the comments here
return (
{post.comments.map(comment => ({ comment }))}
);
};
Post.propTypes = {
getPost: PropTypes.func.isRequired,
group: PropTypes.object.isRequired
};
const mapStateToProps = state => ({
post: state.post
});
export default connect(mapStateToProps, { getPost })(Post);
You can access nested object with some tricks using redux, we have use this way in our prod env for some time.
First the reducer (you can make this reducer even more complex)
const LocalStorageReducer = createReducer<Store['localStorage']>(
new LocalStorage(),
{
saveLocalStorageItem(state: LocalStorage, action: any) {
return {...state, [action.payload.item]: action.payload.value}; // <= here
},
}
);
For Actions
export const actions = {
saveLocalStorageItem: (payload: InputAction) => ({type: 'saveLocalStorageItem', payload}),
};
For the type InputAction
export class InputAction {
item: string;
value: string | Array<string> | null | boolean;
constructor() {
this.item = '';
this.value = null;
}
}
For the handler in component
this.props.saveLocalStorage({ item: 'loading', value: false });
In this way you can go one way done to the nested redux store.
For complex (4-5 levels) and multiple (> 2 times) data structure, there are other ways, but in most situations, it's good enough.
If I fetch this array of restos with redux:
[{
res_id: Int,
res_name: String,
res_category: String,
res_category_id: Int,
city_id: Int
}]
My action looks something like this:
export const getrestos = () => {
const resData = await response.json();
dispatch({
type: GET_RESTOS,
payload: resData
});
};
};
export const setFilters = filterSettings => {
console.log(filterSettings);
return { type: SET_FILTERS, filters: filterSettings };
};
And this is my reducer:
import { GET_RESTOS, SET_FILTERS } from '../actions/restos';
const initialState = {
restoList: [],
filteredRestos: []
};
export default (state = initialState, action) => {
switch (action.type) {
case GET_RESTOS:
return {
restoList: action.payload
}
case SET_FILTERS:
const appliedFilters = action.filters;
const updatedFilteredRestos = state.restoList.filter(resto => {
if (appliedFilters.cityID || resto.city_id) {
resto => resto.city_id.indexOf(cityID) >= 0
return { ...state, filteredRestos: updatedFilteredRestos };
}
});
return { ...state, filteredRestos: updatedFilteredRestos };
default:
return state;
}
};
I have touchable categorys in a page, and when i touch one i want to fetch the corresponding restos for that category and show them in a flatlist. Apart from that i want to have a search bar that when I type I want to show restos by res_name and/or by res_category.
Ive tried to create selectors, but I dont understand how, i dont need an specific approach, but the most clean or efficient as possible.
Thanks in advance if anyone can give me a hint or solution!
EDIT
The problem is im getting undefined in updatedFilteredRestos.
Your reducers should be clean, dumb and all they do should be returning objects. This makes your components more testable and errors easier to catch. In my opinion, this is a perfect use-case for reselect. Here's a medium article: https://medium.com/#parkerdan/react-reselect-and-redux-b34017f8194c But the true beauty of reselect is that it will memoize for you, i.e. if your states don't change, it uses a cached version of the data.
Anyway, you should clean up your restoReducer to something to this effect.
import { GET_RESTOS, SET_FILTERS } = "../actions/restos";
const initialState = {
restoList: [],
filteredRestos: []
};
const restoReducer = (state = initialState, action) => {
switch(action.type) {
case GET_RESTOS:
return { ...state, restoList: action.payload };
case SET_FILTERS:
return { ...state, filteredRestos: action.payload };
default:
return state;
}
}
Then write your filtered resto selector:
// ../selectors/restos
import { createSelector } from "reselect";
// First, get your redux states
const getRestos = (state) => state.restos.restoList;
const getFilteredRestos = (state) => state.restos.filteredRestos;
// Next, create selectors
export const getFilteredRestoList = createSelector(
[getRestos, getFilteredRestos],
(restoList, filteredRestos) => {
// need to check for non-empty filters
// if it is, simply return the unfiltered `restoList`
if(!Array.isArray(filteredRestos) || !filteredRestos.length)
return restoList || [];
// If you do have valid filters, return filtered logic
return restoList.filter(r => filteredRestos.some(f => f.cityID === r.city_id));
);
Then, use this selector in your components:
// ../components/my-app
import { getFilteredRestoList } from "../selectors/restos";
// hook it up to your `mapStateToProps` as you would a normal state
// except this time, it's a special selector
const mapStateToProps = (state, ownProps) => {
restoList: state.restos.restoList,
filteredRestos: state.restos.filteredRestos,
filteredRestoList: getFilteredRestoList(state) //<-- this is your selector
}
Then inside your component, just reference it: this.props.filteredRestoList.
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.
Trying to mess around with react and redux to fetch a list of files from an API.
When looking in the react dev tools I can see my data there but it is not being rendered.
actions.js
export const requestFiles = ({
type: REQUEST_FILES,
});
export const receiveFiles = (json) => ({
type: RECEIVE_FILES,
files: json,
receivedAt: Date.now()
});
export const fetchFiles = (dispatch) => {
dispatch(requestFiles);
return fetch('/api/files')
.then(response => response.json())
.then(json => dispatch(receiveFiles(json)))
};
The action gets data from JSON
reducer.js
const files = (state = {
isFetching: false,
items: []
}, action) => {
switch (action.type) {
case REQUEST_FILES:
return {
...state,
isFetching: true,
};
case RECEIVE_FILES:
return {
...state,
isFetching: false,
items: action.files,
lastUpdated: action.receivedAt
};
default:
return state
}
};
const filesUploaded = (state = { }, action) => {
switch (action.type) {
case RECEIVE_FILES:
case REQUEST_FILES:
return {
...state,
items: files(state[action], action)
};
default:
return state
}
};
const rootReducer = combineReducers({
filesUploaded
});
export default rootReducer
App.js
class App extends Component {
static propTypes = {
files: PropTypes.array.isRequired,
isFetching: PropTypes.bool.isRequired,
dispatch: PropTypes.func.isRequired
};
componentDidMount() {
const {dispatch} = this.props;
dispatch(fetchFiles);
}
handleChange = nextSubreddit => {
};
render() {
const {files, isFetching} = this.props;
const isEmpty = files.length === 0;
console.log(`Files is empty ${isEmpty}`);
return (
<div>
<h1>Uploadr</h1>
{isEmpty
? (isFetching ? <h2>Loading...</h2> : <h2>No files.</h2>)
: <div style={{opacity: isFetching ? 0.5 : 1}}>
<Files files={files}/>
</div>
}
</div>
)
}
}
const mapStateToProps = state => {
const {uploadedFiles} = state;
const {isFetching, items: files} = uploadedFiles
|| {isFetching: true, items: []};
console.log(files);
return {
files,
isFetching,
}
};
The data being received in the action but I am not sure if it is getting stored or if the problem is accessing it from the redux store.
The files property is still zero on the App component as shown in the screenshot above.
Any ideas?
Delete your filesUploaded reducer. You don't need it. Instead, just use the files reducer:
const rootReducer = combineReducers({
files,
});
Please note, the slice of state you are interested in will be called files. Change your mapStateToProps function to this:
const mapStateToProps = state => {
const {isFetching, items: files} = state.files
console.log(files);
return {
files,
isFetching,
}
};
You can see here, we grab the files slice of state and pass that into your component.
Your filesUploaded reducer does not make any sense. I'm not sure what filesUploaded is even supposed to be doing. Your files reducer looks like a normal reducer. It seems like you could just delete filesUploaded and everything would be fine.
In particular, filesUploaded is calling files(state[action], action). action is an object. What is state[SOME_OBJECT] supposed to be? Because it's being parsed as state['[object Object]'] which is surely undefined and never would become defined.
Your files reducer also has an items parameter that is just never used. A reducer should only have two parameters: state and action. Drop the items parameter.
Your mapStateToProps is looking for state.uploadedFiles, but your reducer is called filesUploaded. It should be state.filesUploaded (or if you replace it with the files reducer, just state.files).
mapStateToProps will not need || {isFetching: true, items: []} since you have an initial state on your files reducer.