I am building a ETH portfolio tracker using ethplorer's API with React, redux-react and thunk middleware on the frontend. The main component of the store is an array of objects (tokens). You could see its reducer and actions below:
import {createToken, initializeTokens, deleteToken, replaceTokens} from '../services/tokens'
const tokenReducer = (state = [], action) => {
switch (action.type) {
case 'ADD_TOKEN':
return state.concat(action.data)
case 'ERASE_TOKEN':
return state.filter(token => token.address !== action.data)
case 'INIT_TOKENS':
return action.data
default:
return state
}
}
//Defining Actions
export const addToken = address => {
return async dispatch => {
const newToken = await createToken(address)
dispatch({
type: 'ADD_TOKEN',
data: newToken
})
}
}
export const updateTokens = () => {
return async dispatch => {
const updatedTokens = await initializeTokens()
await replaceTokens(updatedTokens)
dispatch({
type: 'INIT_TOKENS',
data: updatedTokens
})
}
}
export const eraseToken = address => {
return async dispatch => {
await deleteToken(address)
dispatch({
type: 'ERASE_TOKEN',
data: address
})
}
}
export default tokenReducer
Imported functions are being used to fetch tokens from the API and then save them into a local database. Another component of an action is a MarketCap filter (string) that I use to sort tokens according to their market cap (biggest to smallest, smallest to biggest or none). It has a super simple reducer and an action:
const mcReducer = (state='NONE', action) => {
switch (action.type) {
case 'SET_MC_FILTER':
return action.filter
default:
return state
}
}
export const mcFilterChange = filter => {
return {
type: 'SET_MC_FILTER',
filter
}
}
export default mcReducer
The problem begins when I start sorting tokens for display in the React component. While I do not intend to change the tokens state, and only want to change the array of displayed tokens, my tokens state neverthless changes to the sorted out one after I sort by the MC. So what happens is: programm dispatches SET_MC_FILTER => TokenList sorts tokens according to the filter => tokenReducer returns mutated state instead of the old one. I don't understand why it happens, since I don't dispatch any actions that should affect the tokens state here and by default tokenReducer should just return the state that was stored in it. Here is the last piece of code where the problem apparently happens:
const initTokens = useSelector(state => state.tokens)
const mcFilter = useSelector(state => state.mcFilter)
const getDisplayTokens = inTokens => {
switch (mcFilter) {
case 'NONE':
return inTokens
case 'DESCENDING':
return inTokens.sort(compareMCDescending)
case 'ASCENDING':
return inTokens.sort(compareMCAscending)
default:
return inTokens
}}
return(
<div className='token-list'>
{getDisplayTokens(initTokens).map(t =>
<TokenTile token={t} key={t.id}/>
)}
</div>
)
I have tried to track down the exact place where the tokens change with debugger and trace option of redux-devtools, but everywhere the tokenReducer instantly returns the changed state, and I have no idea why. Any bits of help would be greatly appreciated
Array.prototype.sort() mutates arrays in place. You should never try to call .sort() directly on arrays that were read from the Redux state. You must make copies of the arrays and then sort them.
Also, note that you should use our official Redux Toolkit package, which will both eliminate mutations in reducers, and throw errors if you ever try to mutate code outside of reducers.
See https://redux.js.org/tutorials/fundamentals/part-8-modern-redux for a tutorial on how to use RTK correctly.
Related
I am using redux in a project and I want to make a useSelector that would check to see if the values in the redux state are the default values if not it will do a request to the database and update the the state I feel like it is quite complicated though and I am having a hard time getting my head around how I need to do this.
I need to do this because sometimes the correct state is not loaded in the state I am considering just doing a check every time I use useSelector to check if the values are the default values then fetch from the database but I would much prefer to write it a way that would allow to be handled within the redux selector but I can't really grasp I how I need to do it.
const info = useSelector(getInfo)
Ideally I would like the info to be handled when I fetch here
import { SET_USER_DETAILS } from "../constants/userConstants";
const intialState = {
user: { },
};
const userReducer = (state = intialState, action: any) => {
switch (action.type) {
case SET_USER_DETAILS:
return { ...state, user: action.payload };
default:
return state;
}
};
here is what my current reducer looks like what would be the best way to do this as I am finding it a little bit difficult to follow the documentation on the redux website.
You can use redux-thunk. https://redux.js.org/usage/writing-logic-thunks
then your thunk could look something like that:
const thunkFunction = (dispatch, getState) => {
// logic here that can dispatch actions or read state
const currentState = getState() as typeof initialState;
// check if state is default state
if (JSON.stringify(initialState) === JSON.stringify(currentState)) {
fetch(url).then(data => {
dispatch({type: SET_USER_DETAILS, payload: data})
})
}
}
You need first to fetch data in react component:
const MyComponent = () => {
// while fetch is fetching, data will be default state,
// and when fetch is done, that component will automatically
// rerender with new data
const data = useSelector(getInfo);
const dispatch = useDispatch();
useEffect(() => {
dispatch(thunkFunction)
},[])
return <code>{JSON.stringify(data, null, 2)}</code>
}
I did not test it so may require some changes
but in general concept is like this
I have a redux store with 3 reducers:
let reducers = combineReducers({
config: configReducer,
data: dataReducer,
currentState: gameStateRecuder})
let store = createStore(reducers, applyMiddleware(thunkMiddleware));
In each of those reducers the initial store is empty, but once the App component mounts I use useEffect to replace each initial store inside a reducer with the one I receive with axios.get using redux-thunk. It looks like this in every reducer:
let initialState = [];
const SET_STATE = 'SET_STATE';
const configReducer = (state = initialState, action) => {
switch (action.type) {
case SET_STATE: {
return { ...action.state};
}
default:
return state;
}
const setState = (state) => ({ type: SET_STATE, state });
export const getConfigState = () => (dispatch) => {
getAPI.getConfig() //I import getAPI with all the REST API logic//
.then(response => {
dispatch(setState(response));
})
};
And the App trigger is:
const App = (props) => {
useEffect(() => {
props.getConfigState();
props.getDataState();
props.getGameState();
}, []);
return (
//JSX//
);
}
export default compose(connect(null, { getConfigState, getDataState, getGameState }))(App);
However, when the App mounts, I have this mess:
In the end, I get the state of each reducer replaced with the state of the one whose promise resolved the last one. I can try to wrap the app 2 more times with a HOC that does nothing but re-writes a state of the precise reducer, but I would still like to understand what causes a promise to affect other reducers besides the one he needs to effect.
A silly mistake, but maybe someone has the exact same problem - the solution is to give different case names for each reducer - SET_STATE need to become SET_GAME_STATE, SET_CONFIG_STATE, SET_DATA_STATE respectivly. I believe that's because of my misunderstanding on how the dispatch works.
I'm migrating a class-based react system to hooks, and I'm facing some challenges which I can't understand.
Take a look at the snippet below:
async onSearchforOptions(elementId) {
await this.props.onFetchOperatingSystems()
//(3) [{…}, {…}, {…}]
console.log(this.props.operatingSystems)
}
In this method, I am dispatching an action to update the redux state, and right after this I'm logging the result to make sure the information was fetched and updated in the redux state.
The problem is that in an application which uses functional components, the result doesn't seem the same. Instead of updating the redux state and recovering the info right after, it simply doesn't seem to update the state, even if I'm using "await" and the very same actions and reducers the class component is using:
const onSearchforOptions = async (elementId) => {
await props.onFetchOperatingSystems()
//[]
console.log(props.operatingSystems)
}
My connection for both components (the class component and the functional component):
const mapStateToProps = state => {
return {
operatingSystems: state.operatingSystemReducer.operatingSystems
}
}
const mapDispathToProps = dispatch => {
return {
onFetchOperatingSystems: () => dispatch(actions.fetchOperatingSystems())
}
}
export default connect(mapStateToProps, mapDispathToProps)(productsForm)
My actions:
export const fetchOperatingSystemsStart = () => {
return {
type: actionTypes.FETCH_OPERATING_SYSTEMS_START
}
}
export const fetchOperatingSystemsFail = (error) => {
return {
type: actionTypes.FETCH_OPERATING_SYSTEMS_FAIL,
error: error
}
}
export const fetchOperatingSystemsSuccess = (operatingSystems) => {
return {
type: actionTypes.FETCH_OPERATING_SYSTEMS_SUCCESS,
operatingSystems: operatingSystems
}
}
export const fetchOperatingSystems = () => {
return dispatch => {
dispatch(fetchOperatingSystemsStart())
return axios.get(url)
.then(response => {
const fetchedData = []
for (let key in response.data) {
fetchedData.push({
...response.data[key],
id: response.data[key].id
})
}
dispatch(fetchOperatingSystemsSuccess(fetchedData))
})
.catch(error => {
if (error.response !== undefined) dispatch(fetchOperatingSystemsFail(error.response.data))
else dispatch(fetchOperatingSystemsFail(error))
})
}
}
My Reducer:
const initialState = {
operatingSystems: [],
loading: false
}
const fetchOperatingSystemsStart = (state) => {
return updateObject(state, { loading: true })
}
const fetchOperatingSystemsSuccess = (state, action) => {
return updateObject(state, { operatingSystems: action.operatingSystems, loading: false })
}
const fetchOperatingSystemsFail = (state) => {
return updateObject(state, { loading: false })
}
const reducer = (state = initialState, action) => {
switch (action.type) {
case actionTypes.FETCH_OPERATING_SYSTEMS_START: return fetchOperatingSystemsStart(state)
case actionTypes.FETCH_OPERATING_SYSTEMS_SUCCESS: return fetchOperatingSystemsSuccess(state, action)
case actionTypes.FETCH_OPERATING_SYSTEMS_FAIL: return fetchOperatingSystemsFail(state)
default: return state
}
}
export default reducer
updateObject function:
export const updateObject = (oldObject, updatedProperties) => {
const element = {
// The values of the object oldObject are being spread, at the same time the values of
// updatedProperties are (I'm taking out the attributes of both objects with the spread operator).
// In this case, since the names of the attributes are the same,
// the attributes (which were spread) of the first object will have their values replaced
// by the values of the second object's attributes.
...oldObject,
...updatedProperties
}
return element
}
My Goal:
Accoding to the snippet below, my goal is to dynamically search for options and update it in my form, which is in the component state.
const onSearchforOptions = async (elementId) => {
let elementUpdated
switch (elementId) {
case 'operatingSystem': {
await props.onFetchOperatingSystems()
console.log(props.operatingSystems)
elementUpdated = {
'operatingSystem': updateObject(productsForm['operatingSystem'], {
selectValue: {
value: props.selectedElement.operatingSystem ? props.selectedElement.operatingSystem.id : undefined,
label: props.selectedElement.operatingSystem ? props.selectedElement.operatingSystem.name : undefined
},
elementConfig: updateObject(productsForm['operatingSystem'].elementConfig, {
options: props.operatingSystems
})
})
}
break
}
case 'productType': {
await props.onFetchProductTypes()
elementUpdated = {
'productType': updateObject(productsForm['productType'], {
selectValue: {
value: props.selectedElement.productType ? props.selectedElement.productType.id : undefined,
label: props.selectedElement.productType ? props.selectedElement.productType.name : undefined
},
elementConfig: updateObject(productsForm['productType'].elementConfig, {
options: props.productTypes
})
})
}
break
}
default: break
}
const productsFormUpdated = updateObject(productsForm, elementUpdated)
setProductsForm(productsFormUpdated)
}
The props object passed to the render function initially is not going to be mutated; rather the props passed to your component on its next render will be updated. This is more in keeping with the flux architecture. You fire-and-forget an action, the reducer runs, and then your component is re-rendered with new props.
Before, this same thing was happening, but the new props were being assigned to this.props again. Since there's no meaningful "this" anymore, you can't use this pattern. Besides, depending on this behavior is not idiomatically the React way of doing things.
Update:
I think this is like a great number of cases I've also encountered where the React team seemed to overcorrect for a lot of use cases of people handling derived state poorly (see You Probably Don't Need Derived State). I've seen plenty of cases, like yours, where the now-deprecated componentWillReceiveProps lifecycle method solved this problem for class-based components very nicely.
Thankfully, useEffect now gives you something like a replacement. Think about it this way: when props.operatingSystems changes, you want to perform the effect of changing the state of your form. It's an unfortunate double update issue, but you had that before. Here's how you could go about writing that:
const [productsForm, setProductsForm] = useState(...);
useEffect(() => {
// Handle the case where props.operatingSystems isn't initialized?
if (!props.operatingSystems || !props.selectedElement.operatingSystem)
return;
setProductsForm({
...productsForm,
operatingSystem: {
...productsForm.operatingSystem,
selectValue: {
value: props.selectedElement.operatingSystem.id,
label: props.selectedElement.operatingSystem.name
},
elementConfig: {
...productsForm.operatingSystem.elementConfig,
options: props.operatingSystems
}
}
});
}, [props.operatingSystems]);
The way this works is that your effect code is only kicked off whenever your props.operatingSystems value changes since the last render. You can do a similar sort of effect for product types.
Another option which is maybe less elegant is for your async function that kicked off the redux actions to also resolve to a value which you can then use in your state setting code:
const operatingSystems = await props.onFetchOperatingSystems();
// ...now set your state
i usually implements thunks in a functional component like:
`export default connect(mapStateToProps, {fetchOperatingSystems})(productsForm)`
can you try this and comment back.
In my solution which is an ASP.NET Core project with React, Redux, and Kendo React Components I need to return my props as an array. I'm using the Kendo Dropdown widget as below.
<DropDownList data={this.props.vesseltypes} />
However I receive the error of :
Failed prop type: Invalid prop data of type object supplied to
DropDownList, expected array.
So, I checked my returned data from the props.vesseltypes which is an array of as opposed to a flat array.
Here is my code for how this data is returned:
components/vessels/WidgetData.js
import React, { Component } from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { actionCreators } from '../../store/Types';
import { DropDownList } from '#progress/kendo-react-dropdowns';
class WidgetData extends Component {
componentWillMount() {
this.props.requestTypes();
}
render() {
console.log(this.props.vesseltypes)
return (
<div>
<DropDownList data={this.props.vesseltypes} />
</div>
);
}
}
export default connect(
vesseltypes => vesseltypes,
dispatch => bindActionCreators(actionCreators, dispatch)
)(WidgetData);
components/store/Types.js
const requestVesselTypes = 'REQUEST_TYPES';
const receiveVesselTypes = 'RECEIVE_TYPES';
const initialState = {
vesseltypes: [],
isLoading: false
};
export const actionCreators = {
requestTypes: () => async (dispatch) => {
dispatch({ type: requestVesselTypes });
const url = 'api/KendoData/GetVesselTypes';
const response = await fetch(url);
const alltypes = await response.json();
dispatch({ type: receiveVesselTypes, alltypes });
}
}
export const reducer = (state, action) => {
state = state || initialState;
if (action.type === requestVesselTypes) {
return {
...state,
isLoading: true
};
}
if (action.type === receiveVesselTypes) {
alltypes = action.alltypes;
return {
...state,
vesseltypes: action.alltypes,
isLoading: false
}
}
return state;
};
And finally, the reducer is defined in the store
components/store/configureStore.js
const reducers = {
vesseltypes: Types.reducer
};
Controllers/KendoDataController.cs
[HttpGet]
public JsonResult GetVesselTypes()
{
var types = _vesselTypeService.GetVesselTypes();
return Json(types);
}
So, the dropdown widget expects an array, what I return via the store is an array of objects. As such, this can't be used by the dropdown because it's not what it is expecting. My question is, how do I return this as a single array or flat array?
First deconstruct the part that you want to map to a property from your state:
export default connect(
({vesseltypes}) => ({vesseltypes}),
dispatch => bindActionCreators(actionCreators, dispatch)
)(WidgetData);
Then you could just map vesselTypes to an array of strings, since that's what Kendo DropdownList seems to expect:
<div>
<DropDownList data={this.props.vesseltypes.map((vessel) => vessel.TypeName)} />
</div>
Which should result in what you wanted to achieve.
Alternatively you could look into how to implement a HOC to map your objects to values, it's specified in the Kendo docs, or you can checkout the Stackblitz project they've prepared.
It looks like you forgot to extract vesselTypes from the response here
const alltypes = await response.json();
and your console.log shows that, it contains whole response not just vesselTypes array.
EDIT: On top of that your connect seems wrong, you just pass whole state as a prop not extracting the part you need.
I assume you need an array of strings where the value is in key TypeName.
First of all, I would suggest renaming your variables, if there isn't any back-end restriction like how it's returned via fetch.
For example, these:
alltypes => allTypes
vesseltypes => vesselTypes
Regarding the issue, you just need to do a quick transform before passing data into component. Not sure how the drop down component uses the original input data but I would reduce the array into separate variable to create it only once.
Then pass the variable vesselTypeList into component DropDownList.
Last thing is where to do this transform, when result has been retrieved and Redux updates your props via mapStateToProps first argument of connect function.
const getTypeList = (vesseltypes) => {
return vesseltypes.reduce((result, item) => {
result.push(item.TypeName);
return result;
}, []);
}
const mapStateToProps = ({ vesseltypes }) => { vesseltypes: getTypeList(vesseltypes) };
export default connect(
mapStateToProps,
dispatch => bindActionCreators(actionCreators, dispatch)
)(WidgetData);
Cannot work out what is going on here but basically i have a json file which has a load of products. im trying to then render the ones I want
here is my reducer:
export default(state = initialState, action) => {
switch(action.type){
case Types.SHOW_PRODUCTS: {
console.log('here1');
let productsToShow = data.filter(category => category.link === action.category)
const newState = [].concat(productsToShow[0].products)
return newState;
}
default:
console.log('here2');
return state;
}
}
when I log the state in my store, it says that productsToRender is an array of length 5 (this is correct)
however, when I log (this.props) in one of my components, it says that the length is 0
in the above reducer, the console.log('here 1') is the last console log being called, meaning that it is definitely returning products ( that is verified in the store state). so im not sure why it is then wiping it in that component?
in that component I call this
componentWillMount = () => {
this.props.showProducts(this.props.chosenCategory.category)
}
which passes in the chosen category so I now what products to render
however, logging this.props in the render method below, is showing it to be an empty array
of course I can post more code if necessary but any reason for this funky behaviour?
extra info:
interestingly when I do this:
default:
console.log('here2');
return [{name: 'prod'}];
}
and then log this.props, it now contains this array with this object???
The store should be immutable, that is, the value you return should be made immutable.
I assume you are adding only a single array in the store
Try changing the reducer like,
const initialState = [];
export default(state = initialState, action) => {
switch(action.type){
case Types.SHOW_PRODUCTS: {
console.log('here1');
let productsToShow = data.filter(category => category.link === action.category)
let newState = [...state,...productsToShow[0].products]
return newState;
}
default:
console.log('here2');
return state;
}
}