I'm using material ui tabs, and I'm pushing the tabs to URL. I want to push the tab but not refresh the page every-time I change the tab. Here is my code, and I'm using shallow:true but the problem remains:
const tabs = [
{ label: 'Details', value: 'details' },
{ label: 'Purchases', value: 'purchases' },
{ label: 'Money Transfer', value: 'moneyTransfer' },
{label:'User Activity',value:'userActivity'},
{label:'User Logs',value:'userLogs'}
];
const {
query: { UsersId,tab },
} = router
const handleTabsChange = (event: ChangeEvent<{}>, value: string): void => {
router.push({
pathname: `/dashboard/twigUsers/${UsersId}`,query: { tab:value}},undefined,{ shallow: true });
};
<Tabs
indicatorColor="primary"
onChange={handleTabsChange}
scrollButtons="auto"
sx={{ mt: 3 }}
textColor="primary"
value={tab}
variant="scrollable"
>
{tabs.map((tab) => (
<Tab
key={tab.value}
label={tab.label}
value={tab.value}
/>
))}
</Tabs>
{tab === 'details' && (
<UsersDetailsContainer user={user} userId={UsersId}/>
)}
{tab === 'purchases' && <PurchasesListTable userDetailsId={userDetailsId}/>}
{tab === 'moneyTransfer' && <MoneyTransferListTable userDetailsIdMT={userDetailsIdMT} />}
{tab==='userActivity' &&<UserActivity/>}
{tab==='userLogs' &&<UserLogs user={user} userId={twigUsersId}/>}
You need to prevent propagation, else this will actually just propagate to the parent, and will refresh the site.
const handleTabsChange = (event: ChangeEvent<{}>, value: string): void => {
event.preventDefault();
router.push(
{
pathname: `/dashboard/twigUsers/${UsersId}`,
query: { tab: value },
},
undefined,
{ shallow: true }
);
};
check out an implementation in action here
I have changed the whole logic in order to work, I have stored the tabs in redux saga here is the code:
Redux part
// utils
import axios from 'axios';
import produce from 'immer';
import creator from '../../../utils/actionCreator';
import { put, takeLatest } from 'redux-saga/effects';
import { AnyAction } from 'redux';
// consts
export const REQUEST = '#app/tabs/get/getUsersDetailsTabs/REQUEST';
export const SUCCESS = '#app/tabs/get/getUsersDetailsTabs/SUCCESS';
export const FAILURE = '#app/tabs/get/getUsersDetailsTabs/FAILURE';
export const UPDATE_REQUEST = '#app/tabs/get/getUsersDetailsTabs/UPDATE_REQUEST';
export const UPDATE_SUCCESS = '#app/tabs/get/getUsersDetailsTabs/UPDATE_SUCCESS';
export const UPDATE_FAILURE = '#app/tabs/get/getUsersDetailsTabs/UPDATE_FAILURE';
export const CLEAR = '#app/tabs/get/getUsersDetailsTabs/CLEAR';
// initial state
const initalState = {
error: '',
loading: false,
success: false,
tabs:[]
};
// reducer
const reducer = (state = initalState, { payload, ...action }: AnyAction) =>
produce(state, (draft) => {
switch (action.type) {
case REQUEST:
draft.tabs = payload;
break;
case SUCCESS:
draft.loading = false;
draft.tabs = payload;
break;
case FAILURE:
draft.loading = false;
draft.error = payload;
break;
case UPDATE_REQUEST:
draft.loading = true;
break;
case UPDATE_SUCCESS:
draft.loading = false;
draft.success = true;
break;
case UPDATE_FAILURE:
draft.loading = false;
draft.error = payload;
break;
case CLEAR:
draft.loading = false;
draft.error = '';
draft.tabs = [];
break;
default:
break;
}
});
// actions
const actions = {
request: (tabs) => creator(REQUEST, tabs),
success: (tabs) => creator(SUCCESS, tabs),
failure: (tabs) => creator(FAILURE, tabs),
updateRequest: (tabs) => creator(UPDATE_REQUEST, tabs),
updateSuccess: (tabs) => creator(UPDATE_SUCCESS, tabs),
updateFailure: (tabs) => creator(UPDATE_FAILURE, tabs),
clear: () => creator(CLEAR)
};
// sagas
const sagas = {
*getTabs(tabs){
return{
type:REQUEST,
payload:tabs
}
}
};
// watcher
const watcher = function* watch() {
yield takeLatest(REQUEST, sagas.getTabs);
};
// exports
export { actions, watcher };
export default reducer;
Dispatch the tabs
const [value, setValue] = useState('details');
const handleTabsChange = (event: ChangeEvent<{}>,newValue): void => {
console.log(newValue,"new value")
setValue(newValue);
};
useEffect(() => {
dispatch(getTabs.request(
[ { label: 'Details', value: 'details' },
{ label: 'Purchases', value: 'purchases' },
{ label: 'Money Transfer', value: 'moneyTransfer' },
{label:'User Activity',value:'userActivity'},
{label:'User Logs',value:'userLogs'}]
));
},[]);
const { tabs:tabs } = useSelector((state: RootStateOrAny) => state.tabs);
Tabs
<Tabs
indicatorColor="primary"
onChange={handleTabsChange}
scrollButtons="auto"
sx={{ mt: 3 }}
textColor="primary"
value={value}
variant="scrollable"
>
{tabs.map((tab) => (
<Tab
key={tab.value}
label={tab.label}
value={tab.value}
/>
))}
</Tabs>
{value === 'details' && (
<UsersDetailsContainer user={user} userId={twigUsersId}/>
)}
{value === 'purchases' && <PurchasesListTable userDetailsId={userDetailsId}/>}
{value === 'moneyTransfer' && <MoneyTransferListTable userDetailsIdMT={userDetailsIdMT} />}
{value==='userActivity' &&<UserActivity/>}
{value==='userLogs' &&<UserLogs user={user} userId={twigUsersId}/>}
Related
I am having a callback function which updates my array in the SetState and it works fine as I can see in the console log in the handleDaysValueChange all is good. But when I try to access it in another function i.e handleValuesChange the values are not up to date. I am missing a async or something.
import React from 'react';
import PropTypes from 'prop-types';
import AppService from 'services/app-service';
import Enum from 'enum';
import { ApiData } from 'data';
import { isEmpty, boolValue } from 'core/type-check';
import { notifySuccess } from 'core/errors';
import { withDrawerForm } from 'components/_hoc';
import { ExtendedAvatar } from 'components/extended-antd';
import { Tabs } from 'antd';
import { FormTemplateAudit } from '../templates';
import ShiftRosterFormDetails from './shiftroster-form-details';
import Item from 'antd/lib/list/Item';
const { TabPane } = Tabs;
class ShiftRosterForm extends React.Component {
constructor(props) {
super(props);
this.formInputRef = React.createRef();
this.state = {
daysValue:[]
};
this.handleDaysValueChange = this.handleDaysValueChange.bind(this);
}
handleDaysValueChange(daysValues) {
this.setState({ daysValue: daysValues }, () => {
console.log("Data" + this.props.customData);
console.log("Hello World " + JSON.stringify(this.state.daysValue));
});
}
componentDidMount() {
// Update the parent form state with a reference to the
// formInputRef so we can call this later for validation
// before saving the data to the server.
const { onFormStateChange } = this.props;
onFormStateChange({ formInputRef: this.formInputRef.current });
}
//Shift Roster Detail
handleValuesChange = (props, changedValues, allValues) => {
// Update the parent form state with the changed item
// details and mark the form as dirty
const { itemData, onFormStateChange } = this.props;
console.log("Hey" + this.state.daysValue);
onFormStateChange({
isFormDirty: true,
itemData: {
...itemData,
...changedValues,
...this.state.daysValue
}
});
}
render() {
const { itemData, customData } = this.props;
const isSR = (!isEmpty(itemData) && itemData.id > 0);
return (
<Tabs className="bhp-tabs" defaultActiveKey="1" animated={false}>
<TabPane key="1" tab="Details">
<ShiftRosterFormDetails
ref={this.formInputRef}
dataSource={itemData}
onValuesChange={this.handleValuesChange}
handleDaysValueChange={this.handleDaysValueChange}
/>
</TabPane>
<TabPane key="2" tab="Audit" disabled={!isSR}>
<FormTemplateAudit
itemData={itemData}
/>
</TabPane>
</Tabs>
);
}
}
ShiftRosterForm.propTypes = {
itemId: PropTypes.number, // Passed in by the HOC. The loaded Shift Roster id
itemData: PropTypes.object, // Passed in by the HOC. The loaded Shift Roster data
customData: PropTypes.object, // Temporary store to hold the changed Shift Roster
isFormDirty: PropTypes.bool, // Passed in by the HOC. Flags if the parent form is dirty
isLoading: PropTypes.bool, // Passed in by the HOC. Flags if the parent form is loading
daysValue: PropTypes.object,
onFormStateChange: PropTypes.func // Passed in by the HOC. Callback to update the parent form state.
};
ShiftRosterForm.defaultProps = {
itemId: -1,
itemData: {},
customData: {},
isFormDirty: false,
isLoading: false,
daysValue: {},
onFormStateChange() { }
};
const ShiftRosterFormTitle = ({ data }) => {
const name = (!isEmpty(data) && data.id > 0) ? `${data.name}` : 'New Shift Roster';//`${data.name}`
return isEmpty(data)
? <ExtendedAvatar type="icon" size="large" />
: <span><ExtendedAvatar name={name} type="letter" size="large" />{name}</span>
}
const saveShiftRoster = (shiftrosterId, shiftroster, rosterdays) => {
return ApiData.saveShiftRoster(shiftrosterId, shiftroster, rosterdays)
.then(response => {
notifySuccess('Save Successful', 'Site Work Package saved successfully');
return response;
})
.catch(error => {
throw error;
});
}
const saveForm = (formState, setFormState) => {
const { isFormDirty, itemData, customData, formInputRef } = formState;
const typeName = "Dynamic";
const actualType = itemData.type;
let rosterdays = [];
if (actualType !== typeName) {
rosterdays = GetDaysForRoster(itemData);
console.log("My Values" + JSON.stringify(rosterdays));
}
const shiftRosterId = itemData.id;
const isExistingShiftRoster = shiftRosterId > 0;
return new Promise((resolve, reject) => {
if (isExistingShiftRoster && !isFormDirty) {
// No Changes
notifySuccess('Save Successful', 'Site Work Package saved successfully');
resolve(itemData);
}
else {
// Validate and Save
formInputRef.validateFields((error, values) => {
if (!error) {
// Form validated successfully.
// Save form changes
const shiftrosterRecord = saveShiftRoster(shiftRosterId, values, rosterdays);
resolve(shiftrosterRecord);
}
else {
// Form validation error.
// Return data as is.
resolve(itemData);
}
});
}
});
}
const GetDaysForRoster = (itemsData) => {
const result = [];
const keys = Object.keys(itemsData);
for (const k in keys) {
if (Number(k) == k) {
result[k] = itemsData[k]
}
}
return result.filter(function (el) { return el != null });
}
const WrappedShiftRosterForm = withDrawerForm({
containerClassName: 'bhp-equipment-type-form',
title: (record) => <ShiftRosterFormTitle data={record} />,
onLoad: (itemId, setFormState) => ApiData.getShiftRoster(itemId),
onSave: (formState, setFormState) => { return saveForm(formState, setFormState); },
canView: () => AppService.hasAccess({ [Enum.SecurityModule.EquipmentTypeDetails]: [Enum.SecurityPermission.Read] }),
canCreate: () => AppService.hasAccess({ [Enum.SecurityModule.EquipmentTypeDetails]: [Enum.SecurityPermission.Create] }),
canUpdate: () => AppService.hasAccess({ [Enum.SecurityModule.EquipmentTypeDetails]: [Enum.SecurityPermission.Update] })
})(ShiftRosterForm);
WrappedShiftRosterForm.propTypes = {
containerClassName: PropTypes.string,
itemId: PropTypes.number,
visible: PropTypes.bool,
onSave: PropTypes.func,
onClose: PropTypes.func
};
WrappedShiftRosterForm.defaultProps = {
containerClassName: null,
itemId: -1,
visible: false,
onSave() { },
onClose() { }
};
export default WrappedShiftRosterForm;
//ShiftRosterFormDetails
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { ApiData } from 'data';
import { Form, Input, Select, Button, space, InputNumber } from 'antd';
import ShiftDays from './shiftdays'
const ShiftRosterFormDetails = ({ form, dataSource, onValueChange, handleDaysValueChange }) => {
const { getFieldDecorator } = form;
const formLayout = {
labelCol: {
xs: { span: 24 },
sm: { span: 6 },
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 12 },
},
};
console.log("My datasource" + dataSource.shiftRosterDays);
//const daysRoster = dataSource.shiftRosterDays || [{ daysIn: 1, daysOut: 1, category: "Day Shift" }];
const [inputList, setInputList] = useState([{ daysIn: 1, daysOut: 1, category: "Day Shift" }]);
const [selectedType, setSelectedType] = useState(dataSource.type || 'Fixed Single');
const [isTotalDaysRequired, SetTotalDaysRequired] = useState(dataSource.type === 'Dynamic' ? true : false);
const [isTotalDaysRequiredMessage, setIsTotalDaysRequiredMessage] = useState(dataSource.type === 'Dynamic' ? 'Please enter the Total Days' : '');
const handleTypeChanged = (value, e) => {
setSelectedType(value);
if (value === "Dynamic") {
SetTotalDaysRequired(true);
setIsTotalDaysRequiredMessage('Please enter the Total Days');
}
if (value === "Fixed Single") {
if (inputList.length > 1) {
const list = [...inputList];
console.log("Total" + inputList.length);
list.splice(1, inputList.length);
setInputList(list);
console.log("My List" + JSON.stringify(list));
console.log("Input List" + JSON.stringify(inputList));
handleDaysValueChange(list);
}
}
else {
SetTotalDaysRequired(false);
setIsTotalDaysRequiredMessage('');
}
};
return (
<div className='bhp-equipment-type-form-details bhp-content-box-shadow'>
<Form {...formLayout}>
<Form.Item label='Name' hasFeedback>
{getFieldDecorator('name', {
initialValue: dataSource.name,
rules: [{
required: true,
message: 'Please enter the Name'
}],
})(
//disabled={dataSource.id > 0}
<Input placeholder='Name' />
)}
</Form.Item>
<Form.Item label='Status' hasFeedback>
{getFieldDecorator('status', {
initialValue: dataSource.status,
rules: [{
required: true,
message: 'Please enter the Status'
}],
})(
<Select>
<Select.Option value="Active">Active</Select.Option>
<Select.Option value="InActive">InActive</Select.Option>
</Select>
)}
</Form.Item>
<Form.Item label='Type' hasFeedback>
{getFieldDecorator('type', {
initialValue: dataSource.type || 'Fixed Single',
rules: [{
required: true,
message: 'Please select the Type'
}],
})(
<Select onChange={handleTypeChanged}>
<Select.Option value="Fixed Single">Fixed Single</Select.Option>
<Select.Option value="Fixed Multiple">Fixed Multiple</Select.Option>
<Select.Option value="Dynamic">Dynamic</Select.Option>
</Select>
)}
</Form.Item>
<Form.Item label='Total Days' hasFeedback style={selectedType === 'Dynamic' ? { display: '' } : { display: 'none' }}>
{getFieldDecorator('totaldays', {
initialValue: dataSource.totalDays,
rules: [{
required: isTotalDaysRequired,
message: isTotalDaysRequiredMessage
}],
})(
<InputNumber min={1} max={365} />
)}
</Form.Item>
<ShiftDays inputList={inputList} setInputList={setInputList} selectedType={selectedType} handleDaysValueChange={handleDaysValueChange} getFieldDecorator={getFieldDecorator} />
</Form>
</div>
)};
const onFieldsChange = (props, changedFields, allFields) => {
if (props.onFieldsChange) {
props.onFieldsChange(props, changedFields, allFields);
}
};
const onValuesChange = (props, changedValues, allValues) => {
if (props.onValuesChange) {
props.onValuesChange(props, changedValues, allValues);
}
};
ShiftRosterFormDetails.propTypes = {
form: PropTypes.object,
dataSource: PropTypes.object,
onFieldsChange: PropTypes.func,
onValuesChange: PropTypes.func
};
ShiftRosterFormDetails.defaultProps = {
form: {},
dataSource: {},
onFieldsChange() { },
onValuesChange() { }
};
export default Form.create({
onValuesChange,
onFieldsChange
})(ShiftRosterFormDetails);
I created an Expo project and I am setting some data to LocalStorage using AsyncStorage.
I want to reload that data on App start and want to assign that data to context/State.
How I can achieve that any one can help me to solve this problem.
here is my code of app.js
import React from 'react';
import { Asset } from 'expo-asset';
import * as Font from 'expo-font';
import { AppLoading } from 'expo';
....
const client = new ApolloClient({
// link: new HttpLink({
uri:'',
request: (operation) => {
getToken().then(token=>{
console.log('APIToken:', token);
operation.setContext({
headers: {
'api-token': token ? `${token}` : ''
}
})
});
},
//}),
cache: new InMemoryCache(),
});
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fffef9',
},
});
export default class App extends React.Component {
constructor(props) {
super(props);
this.updateContext=(key, value)=>{
this.setState({
[key]: value
});
};
this.updateStack=(stack)=>{
this.setState({
activeStack: stack
});
};
this.loadUsers=(users, _activeUser)=>{
console.log('Users:', users);
this.setState({
users: users,
activeUser: _activeUser
});
};
this.logOutUser=()=>{
this.setState({
activeUser: {},
users:[],
activeStack:'Auth'
});
};
this.state = {
isReady: false,
activeUser:{},
users: [],
activeStack:'Auth',
updateStack: this.updateStack,
updateContext: this.updateContext,
loadUsers: this.loadUsers,
logOutUser: this.logOutUser
};
}
render() {
if (!this.state.isReady) {
return (
<AppLoading
startAsync={this._cacheResourcesAsync}
onFinish={(res) => {this.setState({ isReady: true })}}
onError={console.warn}
/>
); }
return (
<ApolloProvider client={client}>
<SafeAreaProvider>
<SafeAreaView forceInset={{ bottom: 115 }} style={styles.container}>
{Platform.OS === 'ios' && <StatusBar barStyle="default" />}
<AppContext.Provider value={this.state}>
<AppNavigator/>
</AppContext.Provider>
</SafeAreaView>
</SafeAreaProvider>
</ApolloProvider>
);
}
async _cacheResourcesAsync() {
await Promise.all([
Asset.loadAsync([
// require('./assets/images/robot-dev.png'),
]),
Font.loadAsync({
'body': require('./assets/fonts/Archivo-Regular.ttf'),
'body-bold': require('./assets/fonts/Archivo-SemiBold.ttf'),
'headline': require('./assets/fonts/Nunito-Medium.ttf'),
'headline-bold': require('./assets/fonts/Nunito-Bold.ttf'),
'headline-extra-bold': require('./assets/fonts/Nunito-ExtraBold.ttf'),
})
]);
}
}
I have a function to loaduser() in a separate file, where i am loading users from localstorage and returning a promise. You can tell me where i should call loadUser() and handle its promise to loadState.
Thanks
I had the same problem. My solution was create another Component.
My App.js:
export default () => {
const [isReady, setIsReady] = useState(false);
const loadAssetsAsync = async () => {
const imageAssets = cacheImages([
require('./assets/images/bg_screen1.jpg'),
require('./assets/images/bg_screen2.jpg'),
require('./assets/images/bg_screen3.jpg'),
require('./assets/images/bg_screen4.jpg'),
require('./assets/images/user-cool.png'),
require('./assets/images/user-hp.png'),
require('./assets/images/user-student.png'),
require('./assets/images/avatar1.jpg'),
]);
const fontAssets = cacheFonts([
...vectorFonts,
{ georgia: require('./assets/fonts/Georgia.ttf') },
{ regular: require('./assets/fonts/Montserrat-Regular.ttf') },
{ light: require('./assets/fonts/Montserrat-Light.ttf') },
{ bold: require('./assets/fonts/Montserrat-Bold.ttf') },
{ UbuntuLight: require('./assets/fonts/Ubuntu-Light.ttf') },
{ UbuntuBold: require('./assets/fonts/Ubuntu-Bold.ttf') },
{ UbuntuLightItalic: require('./assets/fonts/Ubuntu-Light-Italic.ttf') },
]);
await Promise.all([...imageAssets, ...fontAssets]);
};
if (!isReady) {
return (
<AppLoading
startAsync={loadAssetsAsync}
onFinish={() => setIsReady(true)}
onError={console.warn}
/>
);
}
return <RootNavigator />;
};
My RootNavigator:
export default () => {
const [loaded, setLoaded] = useState(false);
const [init, setInit] = useState(false);
useEffect(() => {
const fetchData = async () => {
const type = await getUserType()
const name = await getName()
const UID = await getUID()
const token = await getToken()
if (!type || !name || !UID || !token) {
inital = 'Login'
} else if (type === 'CLIENT') {
inital = 'Client'
} else if (type === 'EMPLOYEEE') {
inital = 'Employee'
}
setInit(inital)
}
fetchData()
}, [])
function returnRoute() {
AppNavigator = createStackNavigator({
Login: {
screen: LoginScreen,
navigationOptions: {
headerShown: false
}
},
Client: {
screen: DrawerNavigator,
navigationOptions: {
headerShown: false,
},
},
StoreBooking: StoreBookingScreen,
StoreContract: StoreContractScreen,
IndexContracts: IndexContractsScreen,
ShowActivity: ShowActivityScreen,
ShowUniqueBooking: ShowUniqueBookingScreen,
Addresses: ShowAddressesScreen,
},
{
initialRouteName: init,
defaultNavigationOptions: {
headerStyle: {
backgroundColor: cli.Client.b1,
},
headerTintColor: '#fff',
headerTitleStyle: {
fontWeight: 'bold',
},
}
})
RootNavigator = createAppContainer(AppNavigator);
return <RootNavigator />
}
return (
loaded && init ? (
<Awaiter />
)
:
returnRoute()
)
}
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 have implemented a MobX store in my React-Native app to keep track if a user is being followed or unfollowed. The follow/unfollow is registering, but the MobX store is not being updated. I am trying to update it directly with the this.follows.items[index] = { ...user, isFollowing: !user.isFollowing } but for some reason the store does not trigger an update.
Here is the View Component
#observer
class FollowsListView extends Component<Props> {
follows =
this.props.followType === 'followers'
? followsStore.getFollowersListLoader(this.props.userId)
: followsStore.getFollowingListLoader(this.props.userId);
componentDidMount = () => {
this.follows.lazyLoad();
};
render() {
return (
<>
<AppHeader
title={
this.props.followType === 'followers' ? 'FOLLOWERS' : 'FOLLOWING'
}
/>
<FlatList
contentContainerStyle={{ padding: 15 }}
data={this.follows.items}
keyExtractor={this.getId}
onEndReached={this.follows.loadMore}
onEndReachedThreshold={0.2}
onRefresh={this.follows.loadFromStart}
refreshing={this.follows.isLoading}
renderItem={this.renderFollows}
/>
</>
);
}
private getId = (user: { id: string }) => user.id;
renderUserActionButton(user: UserContainer) {
console.log(user);
return (
user.id !== _SessionManager.id && (
<TouchableOpacity
onPress={() => this.openActionMenu(user.following || user.owner)}
>
<Image source={Images.moreDots} />
</TouchableOpacity>
)
);
}
openActionMenu(user: User) {
const name = user.name || user.username;
const followOption = { name: 'follow', title: `Follow #${name}` };
const unfollowOption = { name: 'unfollow', title: `Unfollow #${name}` };
const options = {
customButtons: [user.isFollowing ? unfollowOption : followOption],
title: null,
takePhotoButtonTitle: null,
chooseFromLibraryButtonTitle: null,
};
ImagePicker.showImagePicker(options, ({ customButton }) => {
if (customButton === 'follow') {
this.props.changeIsFollowingUser(user.id, false);
}
if (customButton === 'unfollow') {
this.props.changeIsFollowingUser(user.id, true);
}
const index = this.follows.items.findIndex((user) => user.id);
this.follows.items[index] = { ...user, isFollowing: !user.isFollowing };
});
}
private renderFollows: ListRenderItem<UserContainer> = ({ item: user }) => {
const userId = user.following ? user.following.id : user.id;
return (
<UserRow
actionContent={this.renderUserActionButton(user)}
onPress={() => this.props.navigateTo('ProfilePublic', { userId })}
user={user.following || user.owner}
/>
);
};
}
const mapDispatchToProps = (dispatch: Function): MappedDispatch =>
bindActionCreators(
{
changeIsFollowingUser,
navigateTo,
},
dispatch
);
export default connect(
null,
mapDispatchToProps
)(FollowsListView);
Here is the Follows Store
import ListLoader from 'Network/ListLoader';
import { Follows } from 'Follows/Types';
import _API from 'Network/API';
class FollowsStore {
followers = new Map<string, Follows>();
followersList = new Map<string, ListLoader<Follows>>();
following = new Map<string, Follows>();
followingList = new Map<string, ListLoader<Follows>>();
getFollowersListLoader(userId: string) {
const list = this.followersList.get(userId);
if (list) return list;
const newList = new ListLoader<Follows>({
fetchData: async (params) => {
const url = `users/${userId}/followers`;
const response = await _API.get(url, { params });
return response.data;
},
onLoad: (data) => {
for (const user of data.items) {
this.followers.set(user.id, user);
}
},
});
this.followersList.set(userId, newList);
return newList;
}
getFollowingListLoader(userId: string) {
const list = this.followingList.get(userId);
if (list) return list;
const newList = new ListLoader<Follows>({
fetchData: async (params) => {
const url = `users/${userId}/following`;
const response = await _API.get(url, { params });
return response.data;
},
onLoad: (data) => {
for (const user of data.items) {
this.following.set(user.id, user);
}
},
});
this.followingList.set(userId, newList);
console.log(newList);
return newList;
}
}
const followsStore = new FollowsStore();
export default followsStore;
In MobX in order to change the state you would need to use an action. Set/decorate your openActionMenu as an action or extract the state changing code to another function which you decorate as action to be cleaner.
So, I have a property (fields), within which I wish to change the value of an element (countries). Alerting the value of countries currently displays the value 2, but I want to change the value to 100, so that re-alerting fields.countries.value, after the change, displays the new value.
How do I do this?
import type { State } from '../../common/types';
import DynamicField from './DynamicField';
import R from 'ramda';
import React from 'react';
import buttonsMessages from '../../common/app/buttonsMessages';
import linksMessages from '../../common/app/linksMessages';
import { FormattedMessage } from 'react-intl';
import { ValidationError } from '../../common/lib/validation';
import { connect } from 'react-redux';
import { fields } from '../../common/lib/redux-fields';
import {
Block,
Box,
Button,
Checkbox,
FieldError,
Flex,
Form,
Heading,
Input,
PageHeader,
Pre,
Radio,
Select,
Space,
Title,
View,
} from '../app/components';
// The example of dynamically loaded editable data.
// cato.org/publications/commentary/key-concepts-libertarianism
const keyConceptsOfLibertarianism = [
'Individualism',
'Individual Rights',
'Spontaneous Order',
'The Rule of Law',
'Limited Government',
'Free Markets',
'The Virtue of Production',
'Natural Harmony of Interests',
'Peace',
].map((concept, index) => ({
id: index,
name: concept,
}));
// Proof of concept. Country list will be read from firebase
const countryArray = [
{ label: 'Select Country', value: 0 },
{ label: 'France', value: 2 },
{ label: 'England', value: 4 },
{ label: 'Swizterland', value: 8 },
{ label: 'Germany', value: 16 },
{ label: 'Lithuania', value: 32 },
{ label: 'Romania', value: 64 },
].map((countryName, index) => ({
id: index,
name: countryName,
}));
// Dynamically create select list
const countryOptions = [];
countryArray.map(countryItem =>
countryOptions.push({ label: countryItem.name.label, value: countryItem.name.value }),
);
// Proof of concept. Country list will be read from firebase
const cityArray = [
{ label: 'Select City', value: 0 },
{ label: 'London', value: 50 },
{ label: 'Paris', value: 75 },
].map((cityName, index) => ({
id: index,
name: cityName,
}));
// Dynamically create select list
const cityOptions = [];
cityArray.map(cityItem =>
cityOptions.push({ label: cityItem.name.label, value: cityItem.name.value }),
);
// Proof of concept. Country list will be read from firebase
const gymArray = [
{ label: 'Select Gym', value: 0 },
{ label: 'Virgin Sport', value: 23 },
{ label: 'Sports Direct', value: 45 },
].map((gymName, index) => ({
id: index,
name: gymName,
}));
// Dynamically create select list
const gymOptions = [];
gymArray.map(gymItem =>
gymOptions.push({ label: gymItem.name.label, value: gymItem.name.value }),
);
type LocalState = {
disabled: boolean,
error: ?Object,
submittedValues: ?Object,
};
class FieldsPage extends React.Component {
static propTypes = {
fields: React.PropTypes.object.isRequired,
dynamicFields: React.PropTypes.object,
// getCities: React.PropTypes.object,
};
state: LocalState = {
disabled: false,
error: null,
submittedValues: null,
};
onFormSubmit = () => {
const { dynamicFields, fields } = this.props;
const values = {
...fields.$values(),
concepts: {
...dynamicFields,
},
};
// This is just a demo. This code belongs to Redux action creator.
// Disable form.
this.setState({ disabled: true });
// Simulate async action.
setTimeout(() => {
this.setState({ disabled: false });
const isValid = values.name.trim();
if (!isValid) {
const error = new ValidationError('required', { prop: 'name' });
this.setState({ error, submittedValues: null });
return;
}
this.setState({ error: null, submittedValues: values });
fields.$reset();
}, 500);
};
handleSelectedCountryChange = () => {
// Pass in the selected country value to get associated cites
const { fields, getCities } = this.props;
getCities('country', fields.$values());
};
/*
handleSelectedCityChange = (event => {
// Pass in the selected city value to get associated gyms
this.setState({secondLevel: event.target.value});
});
*/
render() {
const { fields } = this.props;
const { disabled, error, submittedValues } = this.state;
return (
<View>
<Title message={linksMessages.fields} />
<PageHeader
description="New clients enter their gym details here."
heading="New user entry form."
/>
<Form onSubmit={this.onFormSubmit}>
<Input
{...fields.name}
aria-invalid={ValidationError.isInvalid(error, 'name')}
disabled={disabled}
label="Your Name"
maxLength={100}
type="text"
/>
<FieldError error={error} prop="name" />
<Heading alt>Key Concepts of Libertarianism</Heading>
<Block>
<Flex wrap>
{keyConceptsOfLibertarianism.map(item =>
<Box mr={1} key={item.id}>
<DynamicField
disabled={disabled}
item={item}
path={['fieldsPage', 'dynamic', item]}
/>
</Box>,
)}
</Flex>
</Block>
<Block>
<Checkbox
{...fields.isLibertarian}
checked={fields.isLibertarian.value}
disabled={disabled}
label="I'm libertarian"
/>
<Checkbox
{...fields.isAnarchist}
checked={fields.isAnarchist.value}
disabled={disabled}
label="I'm anarchist"
/>
</Block>
<Block>
<Flex>
<Radio
{...fields.gender}
checked={fields.gender.value === 'male'}
disabled={disabled}
label="Male"
value="male"
/>
<Space x={2} />
<Radio
{...fields.gender}
checked={fields.gender.value === 'female'}
disabled={disabled}
label="Female"
value="female"
/>
<Space x={2} />
<Radio
{...fields.gender}
checked={fields.gender.value === 'other'}
disabled={disabled}
label="Other"
value="other"
/>
</Flex>
</Block>
<Block>
<Select
{...fields.countries}
disabled={disabled}
label="Countries"
onChange={this.handleSelectedCountryChange}
options={countryOptions}
/>
</Block>
<Block>
<Select
{...fields.cities}
disabled={disabled}
label="Cities"
// onChange={this.handleSelectedCityChange}
options={cityOptions}
/>
</Block>
<Block>
<Select
{...fields.gyms}
disabled={disabled}
label="Gyms"
// onChange={this.handleSelectedCityChange}
options={gymOptions}
/>
</Block>
{/*
Why no multiple select? Because users are not familiar with that.
Use checkboxes or custom checkable dynamic fields instead.
*/}
<Button disabled={disabled} type="submit">
<FormattedMessage {...buttonsMessages.submit} />
</Button>
{submittedValues &&
<Pre>
{JSON.stringify(submittedValues, null, 2)}
</Pre>
}
</Form>
</View>
);
}
}
FieldsPage = fields({
path: 'fieldsPage',
fields: [
'countries',
'cities',
'gyms',
'gender',
'isAnarchist',
'isLibertarian',
'name',
],
getInitialState: () => ({
countries: '0',
cities: '0',
gyms: '0',
gender: 'male',
isAnarchist: false,
isLibertarian: false,
}),
})(FieldsPage);
export default connect(
(state: State) => ({
dynamicFields: R.path(['fieldsPage', 'dynamic'], state.fields),
}),
)(FieldsPage);
=====================================================================
fields.js
/* #flow weak */
import R from 'ramda';
import React from 'react';
import invariant from 'invariant';
import { resetFields, setField } from './actions';
type Path = string | Array<string> | (props: Object) => Array<string>;
type Options = {
path: Path,
fields: Array<string>,
getInitialState?: (props: Object) => Object,
};
const isReactNative =
typeof navigator === 'object' &&
navigator.product === 'ReactNative'; // eslint-disable-line no-undef
// Higher order component for huge fast dynamic deeply nested universal forms.
const fields = (options: Options) => (WrappedComponent) => {
const {
path = '',
fields = [],
getInitialState,
} = options;
invariant(Array.isArray(fields), 'Fields must be an array.');
invariant(
(typeof path === 'string') ||
(typeof path === 'function') ||
Array.isArray(path)
, 'Path must be a string, function, or an array.');
return class Fields extends React.Component {
static contextTypes = {
store: React.PropTypes.object, // Redux store.
};
static getNormalizePath(props) {
switch (typeof path) {
case 'function': return path(props);
case 'string': return [path];
default: return path;
}
}
static getFieldValue(field, model, initialState) {
if (model && {}.hasOwnProperty.call(model, field)) {
return model[field];
}
if (initialState && {}.hasOwnProperty.call(initialState, field)) {
return initialState[field];
}
return '';
}
static lazyJsonValuesOf(model, props) {
const initialState = getInitialState && getInitialState(props);
// http://www.devthought.com/2012/01/18/an-object-is-not-a-hash
return options.fields.reduce((fields, field) => ({
...fields,
[field]: Fields.getFieldValue(field, model, initialState),
}), Object.create(null));
}
static createFieldObject(field, onChange) {
return isReactNative ? {
onChangeText: (text) => {
onChange(field, text);
},
} : {
name: field,
onChange: (event) => {
// Some custom components like react-select pass the target directly.
const target = event.target || event;
const { type, checked, value } = target;
const isCheckbox = type && type.toLowerCase() === 'checkbox';
onChange(field, isCheckbox ? checked : value);
},
};
}
state = {
model: null,
};
fields: Object;
values: any;
unsubscribe: () => void;
onFieldChange = (field, value) => {
const normalizedPath = Fields.getNormalizePath(this.props).concat(field);
this.context.store.dispatch(setField(normalizedPath, value));
};
createFields() {
const formFields = options.fields.reduce((fields, field) => ({
...fields,
[field]: Fields.createFieldObject(field, this.onFieldChange),
}), {});
this.fields = {
...formFields,
$values: () => this.values,
$setValue: (field, value) => this.onFieldChange(field, value),
$reset: () => {
const normalizedPath = Fields.getNormalizePath(this.props);
this.context.store.dispatch(resetFields(normalizedPath));
},
};
}
getModelFromState() {
const normalizedPath = Fields.getNormalizePath(this.props);
return R.path(normalizedPath, this.context.store.getState().fields);
}
setModel(model) {
this.values = Fields.lazyJsonValuesOf(model, this.props);
options.fields.forEach((field) => {
this.fields[field].value = this.values[field];
});
this.fields = { ...this.fields }; // Ensure rerender for pure components.
this.setState({ model });
}
componentWillMount() {
this.createFields();
this.setModel(this.getModelFromState());
}
componentDidMount() {
const { store } = this.context;
this.unsubscribe = store.subscribe(() => {
const newModel = this.getModelFromState();
if (newModel === this.state.model) return;
this.setModel(newModel);
});
}
componentWillUnmount() {
this.unsubscribe();
}
render() {
return (
<WrappedComponent {...this.props} fields={this.fields} />
);
}
};
};
export default fields;