I am currently developing a front end application using react-redux. But I am very new to this language.
So basically I have the following UI
What I am trying to achieve is whenever users increase or decrease the option, it will store to the store procedure, and finally make an API call to backend and calculate pricing.
Before API call, my idea is I will let users to increase/decrease the option and finally when the user is done, i will take that array of object and submit to the api endpoint.
Unfortunately, It seems like the following scenario is failed.
I increase option 1, it will save to the state as an array of object
first time with quantity and optionId [OK]
After that, I will increase the option 2, since it is the new option,
I will push the object to the existing array. [OK]
When I try to increase option 1 again, it has to check whether option
1 is already inside the array, if there is option 1, it will just
increase that option quantity. but my code does not behave that way. [FAILED]
below is my Component
import React, {Component} from 'react';
import {handleIncreaseOption} from '../actions/option';
import {Button, Card, Col, Row, Statistic} from "antd";
import {MinusOutlined, PlusOutlined} from '#ant-design/icons';
import {connect} from 'react-redux';
const {Meta} = Card;
class FlavourCard extends Component {
state = {
quantity: 0,
optionId: this.props.optionId
}
increase = () => {
let count = this.state.quantity + 1;
this.setState({
quantity: count,
optionId: this.props.optionId
}, function(){
console.log('this state before going in', this.state);
this.props.dispatch(handleIncreaseOption(this.state));
});
}
decline = () => {
let count = this.state.count - 1;
if (count < 0) {
count = 0;
}
this.setState({count: count});
console.log(this.state);
}
render() {
const {flavourImg, itemTitle} = this.props;
return (
<Card
hoverable
cover={<img alt="example" className="flavour-img" src={flavourImg}/>}
>
<Meta
title={itemTitle}
style={{textAlign: 'center'}}
description={
<Row justify="start" gutter={12}>
<Col span={10} style={{textAlign: 'right', paddingTop: '6px'}}>
<Button onClick={this.decline} size="small">
<MinusOutlined/>
</Button>
</Col>
<Col span={4}>
<Statistic value={this.state.quantity} style={{fontSize: '10px'}}/>
</Col>
<Col span={10} style={{textAlign: 'left', paddingTop: '6px'}}>
<Button onClick={this.increase} size="small">
<PlusOutlined/>
</Button>
</Col>
</Row>
}
/>
</Card>
)
}
}
function mapStateToProps(state) {
return{
loadingBar: state.loadingBar
}
}
export default connect(mapStateToProps) (FlavourCard)
This is my action class
export const RETRIEVE_OPTIONS = 'RETRIEVE_OPTIONS';
export const INCREASE_OPTIONS = 'INCREASE_OPTIONS';
export function receiveOptions( option ) {
return {
type: RETRIEVE_OPTIONS,
option
}
}
export function handleIncreaseOption ( option ) {
return {
type: INCREASE_OPTIONS,
option
}
}
This is my reducer
import {RETRIEVE_OPTIONS, INCREASE_OPTIONS} from "../actions/option";
export default function option ( state = null , action )
{
switch (action.type) {
case RETRIEVE_OPTIONS:
return {
...state,
...action
}
case INCREASE_OPTIONS:
if ( !state.hasOwnProperty('addOption') ) {
return {
...state,
addOption: [
{
quantity: action.option.quantity,
optionId: action.option.optionId
}
]
}
}
state.addOption.map((opt) => {
if(opt.optionId === action.option.optionId) {
opt.quantity = action.option.quantity;
}else {
let originalAddOption = state.addOption;
originalAddOption.push({
quantity: action.option.quantity,
optionId: action.option.optionId
})
}
return {
...state,
...action
}
})
default:
return state
}
}
I believe that my "INCREASE_OPTIONS" reducer is something wrong, because, the correct logic should be when there is a new optionId, it will add in as a new object, and if the optionId is existing one, it will just increase the entity. For my current code, whenever I make a second option to increase, it will just add in a new object with new quantity value. I have attached the console result below
How can I achieve when there is existing option, just increase/decrease the quantity and if option is newly added, make a new object and push to the array? Thanks in advance
There are a couple problems in the reducer.
The first issue is that you are trying to update the state object directly. This will not work, you have to set state to a new object.
The second issue is how you are using the map function. It looks like you are using it to update a value if it exists, or add a new entry if it does not. You might have to separate that out and first check if it exists, if so do an update, if not add a new element. Then for each opt in the array, you return an object containing the entire state and action, which I don't think is your intention.
Try out something like this in the reducer:
case INCREASE_OPTIONS: {
if ( !state.hasOwnProperty('addOption') ) {
return {
...state,
addOption: [
{
quantity: action.option.quantity,
optionId: action.option.optionId
}
]
}
}
let updated = false;
// For every element, check if we find the id to modify
// Map returns an array. Does not modify in place.
let addOptCopy = state.addOption.map((opt) => {
if(opt.optionId === action.option.optionId) {
opt.quantity = action.option.quantity;
updated = true;
}
return opt;
});
// If nothing was updated, push new element
if(!updated){
addOptCopy.push({
quantity: action.option.quantity,
optionId: action.option.optionId
})
}
// return the new state
return {
...state,
addOption: [...addOptCopy]
}
}
As one of the comments on your post suggested, it may be an over complication to be keeping two states, using the components state plus redux state and keeping them in sync. You can do the increase and decrease within the reducer, and get the state from props in the components by linking it in mapStateToProps.
Lastly, there seems to be a typo in the decrease function, you are setting count in state instead of quantity.
Here is a functional example, you only need to pass id to the increaseOption action creator:
const { Provider, useDispatch, useSelector } = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
const { createSelector } = Reselect;
const initialState = {
data: [
{
id: 1,
},
{
id: 2,
},
],
};
//action types
const INCREASE_OPTIONS = 'INCREASE_OPTIONS';
//action creators
const increaseOption = (id) => ({
type: INCREASE_OPTIONS,
payload: id,
});
const reducer = (state, { type, payload }) => {
if (type === INCREASE_OPTIONS) {
const addOption = state.addOption || [];
const exist = addOption.some(
({ optionId }) => optionId === payload
);
return {
...state,
addOption: exist
? addOption.map((option) =>
option.optionId === payload
? { ...option, quantity: option.quantity + 1 }
: option
)
: addOption.concat({
optionId: payload,
quantity: 1,
}),
};
}
return state;
};
//selectors
const selectData = (state) => state.data;
const selectOption = (state) => state.addOption || [];
const createSelectOption = (id) =>
createSelector([selectOption], (options) => {
const option = options.find(
({ optionId }) => optionId === id
);
return option ? option.quantity : 0;
});
const createSelectItem = (itemId) =>
createSelector([selectData], (data) =>
data.find(({ id }) => id === itemId)
);
const createSelectCardProp = (id) =>
createSelector(
[createSelectOption(id), createSelectItem(id)],
(option, item) => ({ option, item })
);
//creating store with redux dev tools
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducer,
initialState,
composeEnhancers(
applyMiddleware(() => (n) => (a) => n(a))
)
);
const FlavourCard = React.memo(function FlavourCard({
id,
}) {
const dispatch = useDispatch();
const selectProps = React.useMemo(
() => createSelectCardProp(id),
[id]
);
const props = useSelector(selectProps);
return (
<button onClick={() => dispatch(increaseOption(id))}>
id: {props.item.id} count:{props.option}
</button>
);
});
const App = () => {
const data = useSelector(selectData);
return (
<ul>
{data.map(({ id }) => (
<FlavourCard key={id} id={id} />
))}
</ul>
);
};
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reselect/4.0.0/reselect.min.js"></script>
<div id="root"></div>
Related
Using semantic ui ,
Initialize the prop at father component : isFavorite = false
can't catch the value after clickig at icon , how i fill the star to diff color (by add attribute?
styleColor={isFavorite : color:"red" : color:"white"
})
component.js
const ProductDetails = () => {
const {productId} = useParams()
const selectedProd = useSelector((state) => state.allProducts.products.find(product =>
product.id === parseInt(productId)
));
const products = useSelector((state) => state.allProducts.products)
let {isFavorite} = selectedProd
const handleFavoriteSelect =(e)=>{
isFavorite = !isFavorite
selectedProd.isFavorite=isFavorite
dispatch(updateProducts(products,selectedProd))
}
return(
<i onClick={e => handleFavoriteSelect(e)} className="right floated star outline icon" onChange={e=> console.log(!e.target.attributes.value)} value={isFavorite}></i>
)
redux/action.js
export const updateProducts = (products,product) => {
const indexOfItem = products.findIndex(q => q.id === product.id);
if (indexOfItem > -1) {
products[indexOfItem] = product;
}
return {
type: ActionTypes.SET_PRODUCTS,
payload: products
};
};
redux/reducer.js
export const productReducer = (state = initialState, { type, payload }) => {
switch (type) {
case ActionTypes.SET_PRODUCTS:
//take state and update this with payload data
return {...state,products:payload};
default:
return state;
}
}
redux/reducer/index.js
import { combineReducers } from 'redux'
import { productReducer,selectedProductReducer } from './productReducer'
const reducers = combineReducers({
allProducts: productReducer,
product:selectedProductReducer
})
export default reducers
Try the following:
style={isFavorite?{color:"red"}:{color:"white"}}
I would suggest an edit to changing the product as well, you are currently mutating state like you've never read a line of documentation about redux or react:
//toggleFavorite would be much more descriptive
const handleFavoriteSelect =(e)=>{
dispatch(updateProducts(products,{
...selectedProd,
isFavorite:!selectedProd.isFavorite
}))
}
export const updateProducts = (products, product) => {
return {
type: ActionTypes.SET_PRODUCTS,
payload: products.map(
(currentProduct) =>
currentProduct.id === product,id
? product //return changed product
: currentProduct //return original product
),
};
};
That still leaves you with way too much logic in your component and action creator, it would be better to define a TOGGLE_FAVORITE action type with the id as payload and let the reducer handle the logic of toggling favorite.
I have a list of cards that take the state tree.
I have one selector that gets the list of jobs, then two selectors that use that selection to map and combine an object to pass into the card.
function ProductionJobs(props) {
const jobData = useSelector(getDataForProductionJobs);
const dataData = useSelector(getDataForProduction(jobData.map(x=>x.jobsessionkey)));
const matData = useSelector(getMatsForProduction(jobData.map(x=>x.jobsessionkey)));
console.count("renders");
const combined = jobData.map(x=> {
const foundData = dataData.find(y=>y.attachedJobKey===x.jobsessionkey);
const foundMaterial = matData.filter(z=>z.attachedJobkey===x.jobsessionkey);
const obj = {...x}
if(foundData) obj.foundData = foundData;
if(foundMaterial) obj.material = foundMaterial;
return obj;
});
const productionCards = combined.map(x=><ProductionJobCard key={x.jobsessionkey} props={x} />)
return <div className="ProductionJobs">{productionCards}</div>
}
The problem is - this re-renders unnecessarily. Is there a better way of combining this data on the reducer's side, instead of the component?
You can create a container for ProductionJobCard and select combined items in that one using shallowEqual as second argument when filtering matData items.
const {
Provider,
useDispatch,
useSelector,
shallowEqual,
} = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
const { createSelector } = Reselect;
const initialState = {
productionJobs: [
{ jobSessionKey: 1 },
{ jobSessionKey: 2 },
{ jobSessionKey: 3 },
{ jobSessionKey: 4 },
],
data: [{ id: 1, attachedJobKey: 1 }],
mat: [
{ id: 1, attachedJobKey: 1 },
{ id: 2, attachedJobKey: 1 },
{ id: 3, attachedJobKey: 2 },
],
};
//action types
const TOGGLE_MAT_ITEM = 'TOGGLE_MAT_ITEM';
const TOGGLE_DATA_ITEM = 'TOGGLE_DATA_ITEM';
const TOGGLE_JOB = 'TOGGLE_JOB';
//action creators
const toggleMatItem = () => ({ type: TOGGLE_MAT_ITEM });
const toggleDataItem = () => ({ type: TOGGLE_DATA_ITEM });
const toggleJob = () => ({ type: TOGGLE_JOB });
const reducer = (state, { type }) => {
if (type === TOGGLE_MAT_ITEM) {
//toggles matItem with id of 3 between job 1 or 2
return {
...state,
mat: state.mat.map((matItem) =>
matItem.id === 3
? {
...matItem,
attachedJobKey:
matItem.attachedJobKey === 2 ? 1 : 2,
}
: matItem
),
};
}
if (type === TOGGLE_DATA_ITEM) {
//toggles data between job 1 or 3
const attachedJobKey =
state.data[0].attachedJobKey === 1 ? 3 : 1;
return {
...state,
data: [{ id: 1, attachedJobKey }],
};
}
if (type === TOGGLE_JOB) {
//adds or removes 4th job
const productionJobs =
state.productionJobs.length === 3
? state.productionJobs.concat({ jobSessionKey: 4 })
: state.productionJobs.slice(0, 3);
return { ...state, productionJobs };
}
return state;
};
//selectors
const selectDataForProductionJobs = (state) =>
state.productionJobs;
const selectData = (state) => state.data;
const selectMat = (state) => state.mat;
const selectDataByAttachedJobKey = (attachedJobKey) =>
createSelector([selectData], (data) =>
data.find((d) => d.attachedJobKey === attachedJobKey)
);
const selectMatByAttachedJobKey = (attachedJobKey) =>
createSelector([selectMat], (mat) =>
mat.filter((m) => m.attachedJobKey === attachedJobKey)
);
//creating store with redux dev tools
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducer,
initialState,
composeEnhancers(
applyMiddleware(() => (next) => (action) =>
next(action)
)
)
);
const ProductionJobCard = (props) => (
<li><pre>{JSON.stringify(props, undefined, 2)}</pre></li>
);
const ProductionJobCardContainer = React.memo(
function ProductionJobCardContainer({ jobSessionKey }) {
//only one item, no need to shallow compare
const dataItem = useSelector(
selectDataByAttachedJobKey(jobSessionKey)
);
//shallow compare because filter always returns a new array
// only re render if items in the array change
const matItems = useSelector(
selectMatByAttachedJobKey(jobSessionKey),
shallowEqual
);
console.log('rendering:', jobSessionKey);
return (
<ProductionJobCard
dataItem={dataItem}
matItems={matItems}
jobSessionKey={jobSessionKey}
/>
);
}
);
const ProductionJobs = () => {
const jobData = useSelector(selectDataForProductionJobs);
const dispatch = useDispatch();
return (
<div>
<button onClick={() => dispatch(toggleMatItem())}>
toggle mat
</button>
<button onClick={() => dispatch(toggleDataItem())}>
toggle data
</button>
<button onClick={() => dispatch(toggleJob())}>
toggle job
</button>
<ul>
{jobData.map(({ jobSessionKey }) => (
<ProductionJobCardContainer
key={jobSessionKey}
jobSessionKey={jobSessionKey}
/>
))}
</ul>
</div>
);
};
ReactDOM.render(
<Provider store={store}>
<ProductionJobs />
</Provider>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reselect/4.0.0/reselect.min.js"></script>
<div id="root"></div>
You should not combine the data on the reducer because you will essentially copy the data (combined data is essentially a copy of the data you already have). The combined data is a derived value and such values should not be stored in state but calculated in selectors, re calculate when needed by using memoization (not done here) but if you're interested you can see here how I use reselect for memoizing calculations.
At the moment the filter and find are run on each item but since the outcome is the same the component is not re rendered.
I'm building a block based editor from scratch. I have the model as
initialState = {
id,
name,
blocks: [{
id,
content: [{text: '', attributes: []}]
}]
}
In My React Editor is a component that will have this state in the useReducer.
const Editor: React.FC<Props> = ({ document }: Props) => {
const reducer = (state: any, action: any) =>
produce(state, (draft: any) => {
switch (action.operation) {
case ReducerActions.create: {
const index = draft.blocks.findIndex(
(block: any) => block.id === action.block.prevBlock,
);
draft.blocks.splice(index + 1, 0, action.block);
break;
}
case ReducerActions.delete:
draft.blocks = draft.blocks.filter(
(block: any) => block.id !== action.blockId,
);
break;
case ReducerActions.updateContent: {
draft.blocks = draft.blocks.map((block: any) => {
if (block.id === action.blockId) {
return { ...block, content: action.content };
}
return block;
});
break;
}
}
});
const [state, dispatch]: any = useReducer(reducer, document, initialState);
const handleOnInput = (block: any, blockAction: any) => {
switch (blockAction.action) {
case BlockOperations.backSpace: {
const index = state.blocks.findIndex(
(elem: any) => elem.id === block.id,
);
if (index === 0 || index === -1) {
return;
}
const prevBlock = { ...state.blocks[index - 1] };
prevBlock.content = [...prevBlock.content, ...block.content];
dispatch({
operation: ReducerActions.updateContent,
blockId: prevBlock.id,
content: prevBlock.content,
});
dispatch({
operation: ReducerActions.delete,
blockId: block.id,
});
break;
}
}
};
return(
<div>
<Toolbar/>
<div id="editor">
{state.blocks.map((block: any) => (
<div key={block.id} id={block.id}>
<Block
key={block.id}
block={block}
onInput={handleOnInput}
onSelect={handleSelect}
/>
</div>
))}
</div>
</div>
)
}
So I was memoizing Block component and wrapped all the props for block in useCallback to avoid reinitializing.
But the problem started with OnInput backspace case where i need to access the other blocks content and frame the new content. So i need the latest state that forced me to remove the useCallback and that makes all the changes to state rerender the whole block list.
Is there any way that we could split the state in better way and avoid rerendering of all blocks?
One approach that i have in my mind is let the blocks manage their state and only on blur of editable element will update the state in the parent block. But that would affect us in future where we might need collaboration or we could make the api calls directly in the block content to update the particular block. Since the backend schema is structured in such a way that
documentModel = {
id,
name,
blockIds,
}
blockModel = {
id,
type,
content,
}
Thanks for the help.
I have a very weird bug that I'm trying to understand for 1.5 days now. The problem with this bug is, that it is very hard to show it without showing around 2000 lines of code - I tried rebuilding a simple example in a codesandbox but couldn't reproduce the bug.
The bug can be easily described, though:
I have a parent component A, and a child component B. Both are connected to the same redux store and subscribed to a reducer called active. Both components print the exact same activeQuestion state property. Both components are connected to the redux store individually via connect()
I dispatch an action SET_ACTIVE_QUESTION and the components rerender (I'm not sure why each re-render happens) and component B now has the updated state from the store and component A doesn't ... and I can't seem to figure out why that is.
The real application is fairly big but there are a couple of weird things that I observed:
The bug disappears when I subscribe the parent component of A to the active state (Component A is subscribed itself).
The action to change the active question is qued before it is fired with setTimeout(() => doAction(), 0). If I remove the setTimeout the bug disappears.
Here is why I think this question is relevant even without code: How is it even possible that an action is dispatched in the redux store (the first console log is directly from the reducer) and the wrong state is displayed on a subsequent render? I'm not sure how this could even be possible unless its a closure or something.
Update (mapStateToProps) functions:
Component A (wrong state):
const mapStateToProps = (state: AppState) => ({
active: state.active,
answerList: state.answerList,
surveyNotifications: state.surveyNotifications,
activeDependencies: state.activeDependencies,
});
Component B (right state):
const mapStateToProps = (state: AppState) => ({
surveyNotifications: state.surveyNotifications,
active: state.active,
answerList: state.answerList,
activeDependencies: state.activeDependencies,
});
Update:
The state transition is triggered by component B (correct state) with this function:
const goToNextQuestionWithTransition = (
where: string,
shouldPerformValidation?: boolean
) => {
setInState(false);
setTimeout(() => {
props.goToQuestion(where, shouldPerformValidation);
}, 200);
};
Removing the setTimeout removes the bug (but I don't know why)
Update (show reducer):
export const INITIAL_SATE = {
activeQuestionUUID: '',
...
};
export default function (state = INITIAL_SATE, action) {
switch (action.type) {
case actionTypes.SET_ACTIVE_QUESTION:
console.log('Action from reducer', action)
return { ...state, activeQuestionUUID: action.payload };
...
default:
return {...state};
}
}
Update
Component A - correct state
const Survey: React.FC<IProps> = (props) => {
const {
survey,
survey: { tenantModuleSet },
} = props;
const [isComplete, setIsComplete] = React.useState(false);
const classes = useStyles();
const surveyUtils = useSurveyUtils();
console.log('Log from component A', props.active.activeQuestionUUID)
React.useEffect(() => {
const firstModule = tenantModuleSet[0];
if (firstModule) {
props.setActiveModule(firstModule.uuid);
} else {
setIsComplete(true);
}
}, []);
const orderedLists: IOrderedLists = useMemo(() => {
let orderedQuestionList: Array<string> = [];
let orderedModuleList: Array<string> = [];
tenantModuleSet.forEach((module) => {
orderedModuleList.push(module.uuid);
module.tenantQuestionSet.forEach((question) => {
orderedQuestionList.push(question.uuid);
});
});
return {
questions: orderedQuestionList,
modules: orderedModuleList,
};
}, [survey]);
const validateQuestion = (question: IQuestion) => {
...
};
const findModuleForQuestion = (questionUUID: string) => {
...
};
const { setActiveQuestion, setActiveModule, active } = props;
const { activeQuestionUUID, activeModuleUUID } = props.active;
const currentQuestionIndex = orderedLists.questions.indexOf(
activeQuestionUUID
);
const currentModuleIndex = orderedLists.modules.indexOf(activeModuleUUID);
const currentModule = props.survey.tenantModuleSet.filter(
(module) => module.uuid === active.activeModuleUUID
)[0];
if (!currentModule) return null;
const currentQuestion = currentModule.tenantQuestionSet.filter(
(question) => question.uuid === activeQuestionUUID
)[0];
const handleActiveSurveyScrollDirection = (destination: string) => {
...
};
const isQuestionLastInModule = ...
const moveToNextQuestion = (modules: string[], questions: string[]) => {
if (isQuestionLastInModule) {
if (currentModule.uuid === modules[modules.length - 1]) {
props.setActiveSurveyView("form");
} else {
setActiveQuestion("");
setActiveModule(modules[currentModuleIndex + 1]);
}
} else {
console.log('this is the move function')
setActiveQuestion(questions[currentQuestionIndex + 1]);
}
};
const goToQuestiton = (destination: string, useValidation = true) => {
....
moveToNextQuestion(modules, questions);
};
return (
<section className={classes.view}>
{isComplete ? (
<SurveyComplete />
) : (
<div className={classes.bodySection}>
<Module
// adding a key here is nessesary
// or the Module will not unmount when the module changes
key={currentModule.uuid}
module={currentModule}
survey={props.survey}
goToQuestion={goToQuestiton}
/>
</div>
)}
{!isComplete && (
<div className={classes.footerSection}>
<SurveyFooter
tenantModuleSet={props.survey.tenantModuleSet}
goToQuestion={goToQuestiton}
orderedLists={orderedLists}
/>
</div>
)}
</section>
);
};
const mapStateToProps = (state: AppState) => ({
active: state.active,
answerList: state.answerList,
surveyNotifications: state.surveyNotifications,
activeDependencies: state.activeDependencies,
});
const mapDispatchToProps = (dispatch: Dispatch) =>
bindActionCreators(
{
removeQuestionNotification,
setActiveQuestion,
setActiveModule,
setActiveSurveyScrollDirection,
},
dispatch
);
export default connect(mapStateToProps, mapDispatchToProps)(Survey);
Component B (wrong state)
const Question: React.FC<IProps> = (props: IProps) => {
const [showSubmitButton, setShowSubmitButton] = React.useState(false);
const [inState, setInState] = React.useState(true);
const classes = useStyles();
const { question, module, goToQuestion, active } = props;
const notifications: Array<IQuestionNotification> =
props.surveyNotifications[question.uuid] || [];
const answerArr = props.answerList[question.uuid];
const dependency = props.activeDependencies.questions[question.uuid];
useEffect(() => {
/**
* Function that moves to next or previous question based on the activeSurveyScrollDirection
*/
const move =
active.activeSurveyScrollDirection === "forwards"
? () => goToQuestion("next", false)
: () => goToQuestion("prev", false); // backwards
if (!dependency) {
if (!question.isVisible) move();
} else {
const { type } = dependency;
if (type === DependencyTypeEnum.SUBTRACT) {
console.log('DEPENDENCY MOVE')
move();
}
}
}, [dependency, question, active.activeQuestionUUID]);
console.log('Log from component B', active.activeQuestionUUID)
const goToNextQuestionWithTransition = (
where: string,
shouldPerformValidation?: boolean
) => {
// props.goToQuestion(where, shouldPerformValidation);
setInState(false);
setTimeout(() => {
props.goToQuestion(where, shouldPerformValidation);
}, 200);
};
/**
* Questions that only accept one answer will auto submit
* Questions that have more than one answer will display
* complete button after one answer is passed.
*/
const doAutoComplete = () => {
if (answerArr?.length) {
if (question.maxSelect === 1) {
goToNextQuestionWithTransition("next");
}
if (question.maxSelect > 1) {
setShowSubmitButton(true);
}
}
};
useDidUpdateEffect(() => {
doAutoComplete();
}, [answerArr]);
return (
<Grid container justify="center">
<Grid item xs={11} md={8} lg={5}>
<div className={clsx(classes.question, !inState && classes.questionOut)}>
<QuestionBody
question={question}
notifications={notifications}
module={module}
answerArr={answerArr}
/>
</div>
{showSubmitButton &&
active.activeQuestionUUID === question.uuid ? (
<Button
variant="contained"
color="secondary"
onClick={() => goToNextQuestionWithTransition("next")}
>
Ok!
</Button>
) : null}
</Grid>
</Grid>
);
};
const mapStateToProps = (state: AppState) => ({
surveyNotifications: state.surveyNotifications,
active: state.active,
answerList: state.answerList,
activeDependencies: state.activeDependencies,
});
const mapDispatchToProps = (dispatch: Dispatch) =>
bindActionCreators(
{
setActiveQuestion,
},
dispatch
);
export default connect(mapStateToProps, mapDispatchToProps)(Question);
Can you post a copy of the mapStateToProps of both component B and component A? If you are using reselect (or similar libraries), can you also post the selectors definitions?
Where are you putting the setTimeout() call?
If you are sure that there are no side effects within the mapStateToProps then it seems that you are mutating the activeQuestion property somewhere before or after the component B re-renders, assigning the old value. (Maybe you have to search for some assignement in conditions).
Also note that you can not always trust the console log, as it's value can be evaluated at later time the you call it.
I'm trying to build a simple todo-app using react-redux. Problem is when I'm trying to update data, it would not update in view. My code is given below:
actions
export const listTodo = () => { type: actionTypes.LIST_TODO }
export const updateTodo = ( payload, index ) => { type: actionTypes.UPDATE_TODO, payload, index }
Reducers
const INITIAL_STATE = {
all: [{
name: 'init',
status: false,
lastUpdate: new Date().toISOString()
}]
}
const listTodo = ( state, action ) => {...state, all: state.all }
const updateTodo = ( state, action ) => {
const listTodo = {...state, all: state.all }; // init data
// find data
let todo = listTodo.all.filter( (todo, index) => index === action.index );
// update data
todo.name = action.payload.name;
todo.status = action.payload.status;
todo.lastUpdate = new Date().toISOString();
listTodo.all[ action.index ] = todo;
// return data
return {
...state,
all: listTodo.all
}
}
export default ( state = INITIAL_STATE, action) => {
switch( action.type ) {
case LIST_TODO:
return listTodo( state, action );
case UPDATE_TODO:
return updateTodo( state, action );
default:
return state;
}
}
In below code (Components/list.js), I just fetch all todo-list, and then print all list using ShowList.
Components/list.js
import ShowList from '../container/showList';
class TodoList extends Component {
renderTodoList() {
return this.props.all.map( (todo, index) => {
return (
<ShowList key={index} index={index} todo={todo} />
);
});
}
render() {
return <ul> { this.renderTodoList() } </ul>
}
}
const mapStateToProps = ( state ) => { all: state.todo.all };
export default connect( mapStateToProps ) ( TodoList );
In below code (container/showList.js), todo list is shown using <li /> and also have a checkbox, when user click on checkbox, handleCheckbox will trigger, and will update todo-list. I believe data is updated correctly, but it is not updated on html. In browser, todo-list remain same as before.
container/showList.js
class ShowList extends Component {
handleCheckbox = ( ) => {
const { todo, index } = this.props;
todo.status = !todo.status;
todo.lastUpdate = new Date().toISOString();
this.props.onUpdateTodo( todo, index );
}
render() {
const { todo, index } = this.props;
return (
<li> <input type="checkbox" onChange={ this.handleCheckbox } checked={todo.status} /> {todo.name} # {todo.status.toString()} # { todo.lastUpdate } </li>
)
}
}
const mapDispatchToProps = ( dispatch ) => onUpdateTodo: ( todo, index ) => dispatch( actions.updateTodo( todo, index ) )
export default connect(null, mapDispatchToProps) (ShowList);
How can I solve this problem? Thanks in Advance.
Your problem was in your reducers file. Whenever you executed updateToDo() you were not actually updating the the existing todos, you would just add a new property to your state with the new changes. This created layers and layers of properties without actually updating the first-layer. And since your components were only connected to the first-layer, it would never get the updated state.
I've updated a codesandbox for your reference: https://codesandbox.io/s/lively-flower-mwh79
You can update your reducers to something like this and then your code works completely fine:
export default (state = INITIAL_STATE, action) => {
switch (action.type) {
case "LIST_TODO":
return listTodo(state, action);
case "UPDATE_TODO":
return {
...state,
all: state.all.map((todo, index) => {
if (index == action.index) {
return {
...todo,
status: todo.status,
lastUpdate: new Date().toISOString()
};
} else {
return todo;
}
})
};
default:
return state;
}
};
Your problem is in this line,
export const updateTodo = ( payload, index ) => { type: actionTypes.UPDATE_TODO, payload, index }
Redux Action will only take 2 parameters, type and payload respectively.
Here you are passing 3 parameters which is wrong. Remove your index parameter, then your action becomes like this,
export const updateTodo = ( payload, index ) => { type: actionTypes.UPDATE_TODO, payload } //payload = your updated todo list
Pass only update todo list to your action,
this.props.onUpdateTodo( todo );
Finally in your reducer only do this,
return Object.assign(state,action.todo) // This will merge your old state with updated todo list and eventually you will get a updated list.
See more obout Object.assign here