I would like to create a infinite/loop react-native Picker like on the image below.
So, my question is:
When I'm scrolling, how can I make the Picker start again from the first item after reach out the last item?
Here's my code:
render() {
const hourItems = [];
for(var i = 0; i < 24; i++) {
hourItems.push(
<Picker.Item label={i.toString()} value={i} key={i} />
);
}
return(
<ScrollView style={styles.panel}>
<Picker
selectedValue={this.state.hour}
onValueChange={(itemValue, itemIndex) => this.setState({ hour: itemValue })}
>
{hourItems}
</Picker>
</ScrollView>
);
}
Create redux action which increment value of the minutes and hours. Inside that action you can check whether it should be reset, for example:
export const tick = () => (dispatch, getState) => {
const { minute } = getState().clock
if (minute === 59) {
dispatch({ type: RESET_MINUTE})
dispatch({ type: INCREMENT_HOUR})
} else {
dispatch({ type: INCREMENT_MINUTE})
}
}
and in the reducer:
const createReducer = (initialState, handlers) =>
(state = initialState, action) => {
if (handlers.hasOwnProperty(action.type)) {
return handlers[action.type](state, action)
}
return state
}
export default createReducer(initialState, {
INCREMENT_HOUR(state, action) {
return {
...state,
hour: state.hour + 1,
}
},
INCREMENT_MINUTE(state, action) {
return {
...state,
minute: state.minute + 1,
}
},
RESET_MINUTE(state) {
return {
...state,
minute: 0,
}
},
})
then connect your component with the reducer and the update should be automatic :)
Related
I have a Card component that shows a product and then a compare check box. If this is checked then the the item should reflect that in all areas it is shown (with a checked box). This is fine from page to page however there are a few spots that item may be displayed on a carousel and on a list for relative items. So then when that list item is clicked the same item in the carousel on the same 'page' does not update until we refresh. The code is fairly large but we have a button component inside the card component which has a function in the useEffect to watch for changes (which works fine until the item is shown twice on one page) but again it wont update unless we refresh.
I'm sharing the code for the compare button... that's really where I feel the changes are needed:
BUTTON COMPONENT
export interface LikeCompareButtonProps {
products: any[];
}
export default function LikeCompareButton({ products }: LikeCompareButtonProps) {
const dispatch = useDispatch();
const [checked, setChecked] = useState(false);
const checkProducts: any[] = useAppSelector((state) => state?.compareItems?.products)
|| [];
const checkForChange = (products: any) => {
if (compareView) {
setChecked(true);
}
if (!compareView) {
const checked = checkProducts?.find((x) => x.id === products?.id);
if (checked) {
setChecked(true);
} else {
setChecked(false);
}
}
};
const handleChange = (products: {}) => {
if (!checked) {
dispatch(setCompareItem(products));
dispatch(compareItemIncreaseCounter());
setChecked(true);
}
if (checked) {
dispatch(deleteCompareItem(products));
dispatch(compareItemDecreaseCounter());
setChecked(false);
}
getCompareItem();
};
useEffect(() => {
dispatch(getCompareItem());
dispatch(getCompareCounter());
checkForChange(products);
}, [checked]);
// hoping checked here would trigger every time it changes....
// parent component is card one. it maps through an Array -> products
return (
<div>
<button onClick={() => handleChange(products)}>
<div>
{checked && <CheckIcon className="text-white font-bold absolute h-4 w-4" />}
</div>
{!checked ? "Compare" : "Remove"}
</button>
</div>
);
}
CARD COMPONENT import Link from "next/link";
import LikeCompareButton from "../../button/like-compare-button";
export interface SmallProductCardProps {
products: any[];
}
export default function SmallProductCard({products}:
SmallProductCardProps) {
return (
<>
{products?.map((product: any, index: number) => (
<div key={product.id} >
<Link
href={
pathname: `/store/category/d/${product.id}`,
query: { item: product.id }
}}>
<a>
<img
src="http://www.innovativeengsystems.com/wp-c
ontent/uploads/2017/11/no-img-portrait.png"
alt={product.id}
/>
)}
</a>
</Link>
<LikeCompareButton
products={products[index]}
/>
</div>
))}
</>
);
}
HOME: WHERE PRODUCTS IS DEFINED
const Home: NextPage = () => {
const dispatch = useDispatch();
const products: any[] = useAppSelector((state) =>
state?.products?.products?.data);
useEffect(() => {
dispatch(getProductList()); //API RENDERED DATA
}, []);
return (
<>
<SectionLayout hero={true}>
<InnerGridViewLayout colSize={"col-span-4 grid-cols-2 grid-
rows-2 pt-0"}>
<SmallProductCard
products={products}
/>
</InnerGridViewLayout>
</SectionLayout>
</>
);
};
export default Home;
Adding REDUX work
ACTIONS
export const setCompareItem = (products: any) => {
return {
type: SET_COMPARE_ITEM,
payload: products
};
};
export const setCompareItemSuccessful = (products: any) => {
localStorage.setItem("item", JSON.stringify(products.payload));
const existingItems: any[] =
JSON.parse(localStorage.getItem("compareItems")!) ?? [];
existingItems?.push(products.payload);
localStorage.setItem("compareItems",
JSON.stringify(existingItems));
return {
type: SET_COMPARE_ITEM_SUCCESSFUL,
payload: products
};
};
export const getCompareItem = () => {
return {
type: GET_COMPARE_ITEM,
payload: []
};
};
export const getCompareItemSuccessful = () => {
var products: any[] = JSON.parse(localStorage.getItem("compareItems")!);
return {
type: GET_COMPARE_ITEM_SUCCESSFUL,
payload: products
};
};
REDUCER
import {
ERROR_COMPARE,
SET_COMPARE_ITEM,
SET_COMPARE_ITEM_SUCCESSFUL,
GET_COMPARE_ITEM,
GET_COMPARE_ITEM_SUCCESSFUL,
DELETE_COMPARE_ITEM
} from "./actionTypes";
const compareItemsState = {
products: [],
loading: false,
message: ""
};
const reducer = (state = compareItemsState, action) => {
switch (action.type) {
case SET_COMPARE_ITEM:
state = {
...state,
products: [action.payload],
message: "item added"
};
break;
case SET_COMPARE_ITEM_SUCCESSFUL:
state = {
...state,
products: [action.payload],
message: "set item success"
};
break;
case GET_COMPARE_ITEM:
state = {
...state,
products: action.payload,
message: "item found success"
};
break;
case GET_COMPARE_ITEM_SUCCESSFUL:
state = {
...state,
products: action.payload,
message: "found item"
};
break;
case DELETE_COMPARE_ITEM:
state = {
...state,
products: action.payload,
message: "item deleted"
};
break;
case ERROR_COMPARE:
state = {
...state,
compareCounter: state.compareCounter,
message: "Error adding message"
};
break;
default:
state = { ...state };
break;
}
return state;
};
export default reducer;
SAGA.tsx
import { takeEvery, fork, put, all, call } from "redux-
saga/effects";
import {
SET_COMPARE_ITEM,
SET_COMPARE_ITEM_SUCCESSFUL,
DELETE_COMPARE_ITEM,
GET_COMPARE_ITEM_SUCCESSFUL,
GET_COMPARE_ITEM,
} from "./actionTypes";
import {
compareError,
setCompareItem,
setCompareItemSuccessful,
deleteCompareItem,
getCompareItem,
getCompareItemSuccessful,
} from "./actions";
function* setCompareItems(product: any) {
try {
yield put(setCompareItem(product));
} catch (error: any) {
yield put(compareError("Item not added, please try again."));
}
}
function* setCompareItemsSuccessful(products: any) {
try {
yield put(setCompareItemSuccessful(products));
} catch (error: any) {
yield put(compareError("Item not removed, please try
again."));
}
}
function* getCompareItems() {
try {
yield put(getCompareItem());
} catch (error: any) {
yield put(compareError("please try again."));
}
}
function* getCompareItemsSuccessful() {
try {
yield put(getCompareItemSuccessful());
} catch (error: any) {
yield put(compareError("please try again."));
}
}
function* setDeleteItems(products: any) {
try {
yield put(deleteCompareItem(products));
} catch (error: any) {
yield put(compareError("Item removed"));
}
}
export function* watchSetCompareItemSuccess() {
yield takeEvery(SET_COMPARE_ITEM_SUCCESSFUL, setCompareItems);
}
export function* watchSetCompareItem() {
yield takeEvery(SET_COMPARE_ITEM, setCompareItemsSuccessful);
}
export function* watchGetCompareItemSuccess() {
yield takeEvery(GET_COMPARE_ITEM_SUCCESSFUL, getCompareItems);
}
export function* watchGetCompareItem() {
yield takeEvery(GET_COMPARE_ITEM, getCompareItemsSuccessful);
}
export function* watchDeleteCompareItem() {
yield takeEvery(DELETE_COMPARE_ITEM, setDeleteItems);
}
function* compareItemStoreSaga() {
yield all([
fork(watchSetCompareItem),
fork(watchGetCompareItem),
]);
}
export default compareItemStoreSaga;
I want to increase and decrease the counter.counter1 and counter.counter2.innerCount by input value.
Here is the error I found now
I am weak at destructing object etc. and now learning for it.
Could provide me any advice or code? Especially for increment and decrement for innerCount. Much appreciate.
actionCreators.js
import * as actionTypes from "../actionTypes";
export const incrementCounter1 = () => {
return {
type: ActionTypes.INCREMENT_COUNTER_1,
};
};
export const decrementCounter1 = () => {
return {
type: ActionTypes.DECREMENT_COUNTER_1,
};
};
export const incrementByAmount = (amount) => {
return {
type: ActionTypes.INCREMENT_BY_AMOUNT,
amount:amount,
};
};
reducer.js
import * as actionTypes from "../actionTypes";
const INITIAL_STATE = {
counter: {
counter1: 0,
counter2: {
innerCount: 0,
},
},
};
export const Auth = (state = INITIAL_STATE, action) => {
const { type, payload } = action;
let a;
switch (type) {
case ActionTypes.INCREMENT_COUNTER_1:
a = {
...state,
counter: {
...state.counter,
counter1: state.counter.counter1 +=1,
},
};
return a;
case ActionTypes.DECREMENT_COUNTER_1:
a = {
...state,
counter: {
...state.counter,
counter1: state.counter.counter1 -=1,
},
};
return a;
case ActionTypes.INCREMENT_BY_AMOUNT:
a = {
...state,
counter: {
...state.counter,
counter1: state.counter.counter1 +=payload,
},
};
return a;
default:
return state;
}
};
export default Auth;
mainPage.js
import React, { useState } from "react";
import { View, Text, StyleSheet, Button, TextInput } from "react-native";
import {
incrementCounter1,
decrementCounter1,
incrementByAmount,
} from "./states/redux/ActionCreators/auth";
import { connect } from "react-redux";
const Counter = ({
counterRedux,
incrementCounter1,
decrementCounter1,
incrementByAmount,
}) => {
const [amount, setAmount] = useState('');
return (
<View>
<Text style={styles.text}>Input text for changing</Text>
<Button title="INCREMENT" onPress={() => incrementCounter1()} />
<Button title="DECREMENT" onPress={() => decrementCounter1()} />
<View>
<Text style={styles.Atext}>Enter amount to increase:</Text>
<TextInput style={styles.input} value={amount} onChangeText={(a) => setAmount(a)} />
<Text style={styles.Atext}>Amount: {amount}</Text>
<Button title='Add Amount' onPress={(amount)=>incrementByAmount(amount)}></Button>
</View>
<View>
<Text style={styles.Atext}>First Counter: {counterRedux.counter1}</Text>
</View>
</View>
);
};
const mapStateToProps = (state) => {
return {
counterRedux: state.counter,
};
};
const mapDispatchToProps = (dispatch) => {
return {
incrementCounter1: () => dispatch(incrementCounter1()),
decrementCounter1: () => dispatch(decrementCounter1()),
incrementByAmount: (amount) => dispatch(incrementByAmount(amount)),
};
};
const styles = StyleSheet.create({
text: {
fontSize: 25,
},
Atext: {
fontSize: 20,
},
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center",
},
input: {
borderWidth: 1,
borderColor: "#777",
padding: 8,
margin: 10,
width: 200,
},
button: {
backgroundColor: "#fff",
fontSize: 15,
},
});
export default connect(mapStateToProps, mapDispatchToProps)(Counter);
actionTypes.js
export const INCREMENT_BY_AMOUNT = 'INCREMENT_BY_AMOUNT';
export const INCREMENT_COUNTER_1 = 'INCREMENT_COUNTER_1';
export const DECREMENT_COUNTER_1 = 'DECREMENT_COUNTER_1';
Your first issue is here: <Button title='Add Amount' onPress={(amount)=>incrementByAmount(amount)}></Button>.
What you are doing is passing to incrementByAmount the argument passed by onPress which is not at all the amount you expect, but a PressEvent object.
In order to receive the amount you expect, you need to do this: <Button title='Add Amount' onPress={() => incrementByAmount(amount)}></Button> so you get the amount from useState.
Also you definitely don't need to have three actions for your counter, a simpler way to do it would be to have an updateAmount function to which you pass as payload a type that would be "increment" or "decrement", and an amount.
An even simpler way would be to only have an amount and pass either a negative or a positive value to it.
For your increment and decrement buttons, you would simply need to pass 1 or -1 as amount.
Your second issue is that you are mutating your state in your reducer with the += and -= operators.
Here is your fixed reducer (I will let you implement the changes I suggested earlier though):
import * as actionTypes from "../actionTypes";
const INITIAL_STATE = {
counter: {
counter1: 0,
counter2: {
innerCount: 0,
},
},
};
export const Auth = (state = INITIAL_STATE, action) => {
const { type, payload } = action;
switch (type) {
case ActionTypes.INCREMENT_COUNTER_1:
return {
...state,
counter: {
...state.counter,
counter1: state.counter.counter1 + 1,
},
};
case ActionTypes.DECREMENT_COUNTER_1:
return {
...state,
counter: {
...state.counter,
counter1: state.counter.counter1 - 1,
},
};
case ActionTypes.INCREMENT_BY_AMOUNT:
return {
...state,
counter: {
...state.counter,
counter1: state.counter.counter1 + payload,
},
};
default:
return state;
}
};
export default Auth;
I removed the a variable that wasn't needed and changed the += operator to + and the -= operator to - so your state isn't mutated.
Your third issue is that you are destructuring a variable payload while it is called amount in your action creator.
Also don't forget that you are getting a string from <TextInput> and not a number.
Finally you import your action types as actionTypes but use them as ActionTypes.
Eslint throwing eslint(react/prop-types) error despite already declared propTypes. I'm using eslint-plugin-react
I've looked at a couple of other similar problems and as well as the lint rule for the proptype but they don't address my issue.
import React from 'react';
import { View, Text, TouchableHighlight, StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
const PASTEL_PINK = '#dea5a4';
const PASTEL_BLUE = '#779ecb';
const Buttons = ({ onPressStart, onPressPause, onPressReset, onGoing }) => (
<View >
<TouchableHighlight
onPress={onPressStart}
disabled={onGoing}
>
<Text >{START_TIMER}</Text>
</TouchableHighlight>
<TouchableHighlight
onPress={onPressPause}
disabled={!onGoing}
>
<Text >{PAUSE_TIMER}</Text>
</TouchableHighlight>
<TouchableHighlight onPress={onPressReset}>
<Text >{RESET_TIMER}</Text>
</TouchableHighlight>
</View>
);
Buttons.protoTypes = {
onPressStart: PropTypes.func.isRequired,
onPressPause: PropTypes.func.isRequired,
onPressReset: PropTypes.func.isRequired,
onGoing: PropTypes.bool.isRequired,
};
export default Buttons;
Parent component supplying the props
import React from 'react';
import Buttons from './components/Buttons'
import Container from './components/Container';
import Timer from './components/Timer';
import Inputs from './components/Inputs';
import Logo from './components/Logo';
import Buttons from './components/Buttons'
import Header from './components/Header'
export default class Home extends React.Component {
constructor(props){
super(props)
this.state = {
initialMinute: '00',
initialSecond: '00',
minute: '00',
second: '00',
completed: false,
onGoing: false,
}
componentWillMount() {
this.setState({
minute: this.state.initialMinute,
second: this.state.initialSecond,
}
);
}
componentWillUnmount() {
clearInterval(this.interval);
}
startTimer = () => {
console.log("Timer Started")
this.setState(
(prevState) => (
{
completed: false,
onGoing: true,
}
)
)
// start the timer
this.interval = setInterval(
this.decrementTime,
1000
)
}
decrementTime = () => {
if (this.state.second > 0) {
console.log(`second: ${this.state.second}`)
this.setState(
(prevState) => (
{second: prevState.second - 1}
)
)
if (this.props.second < 10) {
this.setState({
second: '0'+this.state.second
});
}
}
else {
if (this.state.minute > 0) {
this.setState(
(prevState) => (
{
minute: prevState.minute - 1,
second: prevState.second + 59,
}
)
)
if (this.props.minute < 10) {
this.setState({
state: '0'+this.state.minute
});
}
}
else {
this.resetTimer();
this.timesUp(true);
}
}
}
pauseTimer = () => {
console.log("Timer stopped")
clearInterval(this.interval);
this.setState({
onGoing: false,
}
)
}
resetTimer = () => {
console.log("Timer is reset")
this.pauseTimer();
this.setState({
minute: this.state.initialMinute,
second: this.state.initialSecond,
}
);
}
timesUp = (bool) => {
this.setState(
(prevState) => (
{
completed: bool,
}
)
)
}
optionPressed = () => {
console.log("Header is pressed")
}
handleMinuteInput = (text) => {
// clamp minute between 0 and 60
// const number = helper.clamp(parseInt(text), 0, 60)
this.setState(
{
initialMinute: text,
}
)
}
handleSecondInput = (text) => {
// const number = helper.clamp(parseInt(text+''), 0, 60)
this.setState(
{
initialSecond: text,
}
)
}
render() {
return (
<Container>
<Header onPress={this.optionPressed}/>
<Logo
slogan={'Get studying, the Pomodoro way!'}
imageSource={'../../assets/pomo-timer-logo-small.png'}
/>
<Timer
minute={this.state.minute}
second={this.state.second}
completed={this.state.completed}
onGoing={this.state.onGoing}
/>
<Buttons
onPressStart={this.startTimer}
onPressPause={this.pauseTimer}
onPressReset={this.resetTimer}
onGoing={this.state.onGoing} // true when not onGoing
/>
<Inputs
inputType={'Work'}
labelColor={PASTEL_BLUE}
handleMinuteInput={this.handleMinuteInput}
handleSecondInput={this.handleSecondInput}
onGoing={this.state.onGoing}
/>
<Inputs
inputType={'Rest'}
labelColor={PASTEL_PINK}
// setTimer={this.setTimer}
handleMinuteInput={this.handleMinuteInput}
handleSecondInput={this.handleSecondInput}
onGoing={this.state.onGoing}
/>
</Container>
)
}
}
I don't expect these error to show up but it does.
'onPressStart' is missing in props validation
'onPressPause' is missing in props validation
'onPressReset' is missing in props validation
'onGoing' is missing in props validation
Replace
Buttons.protoTypes
with
Buttons.propTypes
I have done this mistake too many times
It's propTypes, not protoTypes :)
I have a list and calendar view component in my parent component. For the calendar component I want to be able to push search filters to my url for filtering out the unselected locations. I'm trying to generate a querystring based on the parameters I give to my queryString function, but when I push the queryString to my url I get the following error:
A state mutation was detected between dispatches, in the path locations.calendarLocationList.0. This may cause incorrect behavior. (http://redux.js.org/docs/Troubleshooting.html#never-mutate-reducer-arguments)
I'm not sure what is causing this, since I haven't touched the state during this process.
Parent component, rendering list and calendar view
class LocationShell extends Component<
LocationShellProps & WithNamespaces & RouteComponentProps,
LocationShellState
> {
constructor(props: LocationShellProps & WithNamespaces & RouteComponentProps) {
super(props);
this.state = {
isCalendarView: false,
open: false,
locationIdToDelete: -1,
activePage: 1,
activeSortHeader: undefined,
direction: 'ascending',
searchValue: undefined
};
}
componentDidMount = (
{ loadLocations, loadSessionLocations, loadCalendarListLocations } = this.props,
{ activePage } = this.state
) => {
loadLocations({ page: activePage });
loadSessionLocations();
loadCalendarListLocations();
};
toggleView = () => {
const { isCalendarView } = this.state;
this.setState((prevState) => ({
...prevState,
isCalendarView: !isCalendarView
}))
}
renderListView = () => {
const { locationStatus, locations, selectedLocationId, history, match, pages, t } = this.props;
const { activePage, activeSortHeader, direction } = this.state;
switch (locationStatus) {
case ProgressStatus.InProgress:
return <InitialLoader />
case ProgressStatus.Done:
return (
<DataTableWrapper
// props
/>
)
case ProgressStatus.Error:
return <NoDataFound />
case ProgressStatus.Uninitialized:
return null
}
}
renderCalendarView = ({ calendarListStatus, sessionLocations, calendarListLocations } = this.props) => {
switch (calendarListStatus) {
case ProgressStatus.InProgress:
return <InitialLoader />
case ProgressStatus.Done:
const events = toLocationEvents(sessionLocations!);
return <CalendarView {...this.props} events={events} items={calendarListLocations!} name={'locations'} />
case ProgressStatus.Error:
return <NoDataFound />
case ProgressStatus.Uninitialized:
return null
}
}
render() {
const { pathName, t } = this.props;
const { isCalendarView } = this.state;
return (
<Fragment>
<PageHeader
breadCrumbParts={split(pathName, '/').map(x => t(x))}
title={t('moduleTitle')}
/>
<Button.Group size="mini" style={{ padding: '10px 5px 10px 0px' }}>
<Button positive={!isCalendarView} onClick={this.toggleView}>Lijst</Button>
<Button.Or />
<Button positive={isCalendarView} onClick={this.toggleView}>Kalender</Button>
</Button.Group>
<Button
positive
icon='add'
size="mini"
labelPosition='right'
content="Nieuwe locatie"
onClick={() => this.props.history.push(this.props.match.path + '/create')}
/>
{isCalendarView ? this.renderCalendarView() : this.renderListView()}
</Fragment>
);
}
}
const mapStateToProps = (state: GlobalState) => {
return {
locations: getLocations(state.locations),
calendarListLocations: state.locations.calendarLocationList,
calendarListStatus: state.locations.calendarListStatus,
sessionLocations: state.locations.sessionLocations,
selectedLocation: getSelectedLocation(state.locations),
selectedLocationId: getSelectedLocationId(state.locations),
pages: getAmountPages(state.locations),
locationStatus: state.locations.locationStatus,
sessionLocationStatus: state.locations.sessionLocationStatus,
pathName: getPathName(state.router)
};
};
const mapDispatchToProps = (dispatch: Dispatch) => ({
loadLocations: (queryParams: QueryParams) =>
dispatch(FetchLocations(queryParams)),
loadSessionLocations: () => dispatch(FetchTrainingSessionLocations({})),
loadCalendarListLocations : () => dispatch(FetchCalendarListLocations({})),
clearLocations: () => dispatch(ClearLocations()),
deleteLocation: (id: number) => dispatch(DeleteLocation({ locationId: id }))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(withNamespaces('locations')(LocationShell));
renderCalendarView() is rendering my calendar component
My calendar Component:
interface CalendarViewState {
selectedIds: number[];
}
type CalendarViewProps = {
events: CalendarEvent[];
name: string;
items: CalendarListLocation[];
navigatePush: (values: string) => void;
} & RouteComponentProps
class CalendarView extends Component<CalendarViewProps & WithNamespaces, CalendarViewState> {
state: CalendarViewState = {
selectedIds: []
}
componentDidMount = () => {
const { events, items } = this.props;
const { baseUrl, newEntity } = moduleConstants;
this.setState((prevState) => ({
...prevState,
selectedIds: items.map(x => x._id)
}), () => {
updateQueryString(this.props, { page: 1, locations: [1, 2] })
}
)
}
queryParams(props: CalendarViewProps = this.props) {
return queryParams<QueryParams>(props.location.search);
}
componentDidUpdate = (prevProps: CalendarViewProps, prevState: CalendarViewState) => {
const { selectedIds } = this.state;
console.log()
if (!isEqual(prevState.selectedIds, selectedIds)) {
console.log(this.queryParams())
}
}
handleChange = (id: number) => {
const { selectedIds } = this.state;
this.setState((prevState) => ({
...prevState,
selectedIds: (selectedIds.includes(id) ? selectedIds.filter(x => x !== id) : [...selectedIds, id])
}));
};
render() {
const { events, name, t, items } = this.props
return (
<Grid divided="vertically" padded>
<Grid.Row columns={2}>
<Grid.Column width={4}>
<CalendarSelectionList
name={t(name)}
onSelectionChange={this.handleChange}
selectedIds={this.state.selectedIds}
locations={items.sort((a: CalendarListLocation, b: CalendarListLocation) => a.name.localeCompare(b.name))}
/>
</Grid.Column>
<Grid.Column width={12}>
<div style={{ height: '800px' }}>
<Calendar
events={events.filter(x => this.state.selectedIds.includes(x.id))}
/>
</div>
</Grid.Column>
</Grid.Row>
</Grid>
);
}
}
const mapDispatchToProps = (dispatch: Dispatch) => ({
navigatePush: (path: string) => dispatch(push(path))
});
export default connect(
null,
mapDispatchToProps
)(withNamespaces(['calendar'])(CalendarView));
updateQueryString(this.props, { page: 1, locations: [1, 2] }) gets fired, this function will update the url with the generated queryString
export function queryParams<T>(search: string) {
return (queryString.parse(search) as unknown) as T;
}
export function updateQueryString<T>(props: RouteComponentProps, newValues: T) {
const currentQuery = queryParams<T>(props.location.search);
const newProps = Object.assign(currentQuery, newValues);
props.history.push({
pathname: props.location.pathname,
search: queryString.stringify(filterSearchResults(newProps))
});
}
function filterSearchResults(values: any) {
let obj: any = {};
Object.keys(values).forEach(
key => values[key] && (obj[key] = values[key])
);
return obj;
}
After this, the above error occurs. Why is this error occuring?
The error means that locations.calendarLocationList was mutated, while Redux store is supposed to be immutable.
calendarLocationList is used as items in CalendarView and mutated with items.sort(...) because array sort mutates existing array instead of creating a new one.
This can be fixed with [...items].sort(...).
I'm currently facing a problem where the screen doesnt re-render to load the new added value to array. Even tried with componentWillReceiveProps and trigger a this.forceUpdate() manually doesn't help. The screen only show the new value when I restart the app
this.props.addNewRecord({
recordDetails: { ...this.props.recordDetails, type: this.state.type },
records: this.props.records
});
return (
<View>
{
this.props.records.map((x, i) => {
return (
<View style={{ flexDirection: 'row' }}>
<Text>{`${i}: `}</Text>
<Text>{x.id}</Text>
</View>
);
})
}
</View>
);
const mapStateToProps = ({ account, settings, main }) => {
const year = moment().year();
const month = moment().format('MMM');
return {
records: account.records[year][month].records,
};
};
export default connect(mapStateToProps)(SummaryScreen);
account.records has the following structure
{
2018: {
Jan: {
records: [{...}, {...}]
}
}
}
And below is the adding record part
const addNewRecord = ({ recordDetails, records }) => {
const year = moment(recordDetails.date).year();
const month = moment(recordDetails.date).format('MMM');
const newRec = {
id: uuidv4(),
account_id: recordDetails.account_id,
date: recordDetails.date,
description: recordDetails.description,
amount: recordDetails.result,
type: recordDetails.type
};
update(year, month, newRec, records);
return {
type: ADD_NEW_RECORD,
payload: records
};
};
const update = (year, month, obj, target) => {
[year, month].reduce((r, e, i, a) => {
if (!a[i + 1] && r[e]) {
r[e].records.push(obj);
if (obj.type === 'expense') r[e].totalExpenses = parseFloat(r[e].totalExpenses) + parseFloat(obj.amount);
if (obj.type === 'income') r[e].totalIncome = parseFloat(r[e].totalIncome) + parseFloat(obj.amount);
}
return r[e] = (r[e] || (a[i + 1] ? {} : {
records: [obj],
totalExpenses: obj.type === 'expense' ? parseFloat(obj.amount) : 0,
totalIncome: obj.type === 'income' ? parseFloat(obj.amount) : 0,
type: obj.type
}));
}, target);
};
And Reducer as below:
switch (action.type) {
case ADD_NEW_RECORD:
return { ...state, records: action.payload };
this.props.addNewRecord({
recordDetails: { ...this.props.recordDetails, type: this.state.type },
records: _.cloneDeep(this.props.records)//this.props.records
});
The problem is I've passed this.props.records as records which later gets mutated, and eventually old state get mutated to be the same as new state and that's why Redux unable to spot the differences.
Credits to #Y.Gherbi for spotted this flaw. Hope it helps whoever facing same issue