Related
I created a custom hook (useAuth) to extend the third-party Authentication service Auth0's useAuth0 hook and set some local variables that holds basic user information, such as userId.
I have a master account that can impersonate other accounts. This means that it overrides the userId from my custom hook and it gets propagated throughout the system.
The problem that I'm facing is that whenever I call the impersonate function that changes this hook's inner state, it changes it, but then reinitializes itself. I don't know what is causing this reinitialization. The code is down below.
import { useAuth0 } from '#auth0/auth0-react';
import produce from 'immer';
import { useState, useEffect, useCallback, useReducer, Reducer } from 'react';
import { AccountType, Auth0HookUser, TenantInfo, TenantType } from '../#dts';
type AuthVariants =
| 'INDIVIDUAL_TEACHER'
| 'INSTITUTION_TEACHER'
| 'STUDENT'
| 'SECRETARY'
| 'COORDINATOR'
| 'ADMINISTRATOR';
type AuthTenant = {
accountType: AccountType;
tenantType: TenantType;
employeeId: string;
tenantId: string;
selectedTenant: TenantInfo;
variant: AuthVariants;
mode: 'IMPERSONATION' | 'NORMAL';
user: Auth0HookUser;
};
const defaultAuthTenant: () => AuthTenant = () => ({
accountType: 'teacher',
employeeId: '',
mode: 'NORMAL',
selectedTenant: {
accountType: 'teacher',
tenantType: 'INSTITUTION',
tenantId: '',
},
tenantId: '',
tenantType: 'INSTITUTION',
variant: 'INDIVIDUAL_TEACHER',
user: {
name: '',
nickname: '',
} as any,
});
type Action =
| {
type: 'UPDATE_AUTH';
auth: AuthTenant;
}
| {
type: 'IMPERSONATE';
impersonatedEmployeeId: string;
impersonatedName: string;
accountType: AccountType;
}
| {
type: 'EXIT_IMPERSONATION';
};
type State = {
current: AuthTenant;
original: AuthTenant;
};
const reducer = produce((state: State, action: Action) => {
switch (action.type) {
case 'IMPERSONATE':
console.log('Impersonating');
const selectedTenant =
state.current.user['https://app.schon.io/user_data'].tenants[0];
state.current = {
...state.current,
user: {
...state.current.user,
name: action.impersonatedName,
nickname: action.impersonatedName,
'https://app.schon.io/user_data': {
...state.current.user['https://app.schon.io/user_data'],
userId: action.impersonatedEmployeeId,
},
},
mode: 'IMPERSONATION',
accountType: action.accountType,
employeeId: action.impersonatedEmployeeId,
variant: getVariant(action.accountType, selectedTenant.tenantType),
selectedTenant: {
...state.current.selectedTenant,
accountType: action.accountType,
},
};
return state;
case 'UPDATE_AUTH':
state.current = action.auth;
state.original = action.auth;
return state;
default:
return state;
}
});
export function useAuth() {
const { user: _user, isAuthenticated, isLoading, ...auth } = useAuth0();
const user = _user as Auth0HookUser;
const [selectedTenantIndex, setSelectedTenantIndex] = useState(0);
const [state, dispatch] = useReducer<Reducer<State, Action>>(reducer, {
current: defaultAuthTenant(),
original: defaultAuthTenant(),
});
const impersonate = (
impersonatedEmployeeId: string,
accountType: AccountType,
impersonatedName: string,
) => {
if (!user) {
return;
}
dispatch({
type: 'IMPERSONATE',
accountType,
impersonatedEmployeeId,
impersonatedName,
});
};
const exitImpersonation = useCallback(() => {
dispatch({ type: 'EXIT_IMPERSONATION' });
}, []);
useEffect(() => {
if (isLoading || (!isLoading && !isAuthenticated)) {
return;
}
if (!user || state.current.mode === 'IMPERSONATION') {
return;
}
console.log('Use Effect Running');
const { tenants, userId } = user['https://app.schon.io/user_data'];
const selectedTenant = tenants[selectedTenantIndex];
const { accountType, tenantType } = selectedTenant;
dispatch({
type: 'UPDATE_AUTH',
auth: {
tenantId: selectedTenant.tenantId,
employeeId: userId,
mode: 'NORMAL',
variant: getVariant(accountType, tenantType),
user,
selectedTenant,
accountType,
tenantType,
},
});
}, [
user,
isAuthenticated,
isLoading,
selectedTenantIndex,
state.current.mode,
]);
console.log('State Current', state.current);
return {
isAuthenticated,
isLoading,
impersonate,
exitImpersonation,
setSelectedTenantIndex,
...auth,
...state.current,
};
}
function getVariant(
accountType: AccountType,
tenantType: TenantType,
): AuthVariants {
if (accountType === 'teacher') {
return tenantType === 'INSTITUTION'
? 'INSTITUTION_TEACHER'
: 'INDIVIDUAL_TEACHER';
}
return accountType.toUpperCase() as AuthVariants;
}
See the picture. After I call the impersonate function it sets it to the impersonated mode but re-initializes itself and sets it to the default.
This is what I've tried:
Double Checked that proper dependencies were passed to the useEffect (it is not the one causing the re-initialize).
I was using a useStae before the reducer, and I was calling it via its function vs setting the state directly.
I tried stepping in (debugging) throughout the entire cycle, and didn't find anything.
I went through several SO posts and React dosc to see if I could find any issues, but my blinded eye couldn't see it.
Here's a view where I'm calling it from (See the const {impersonate} = useAuth()) :
import React, { memo, useCallback, useMemo, useState } from 'react';
import { RouteComponentProps } from '#reach/router';
import { Button, Typography } from 'components';
import Skeleton from 'react-loading-skeleton';
import { useAuth } from '../../../../../auth';
import { Tabs, Dialog } from '../../../../../components/';
import { useAllClassesAndTeacherForInstitution } from '../../../../../graphql';
import { useThemeSpacing } from '../../../../../shared-styles/material-ui';
import { AddClassTeacher, ListClassTeacher } from './components';
type TeacherViewRouteProps = {
teacherId: string;
};
export const TeacherView: React.FC<RouteComponentProps<
TeacherViewRouteProps
>> = memo((props) => {
const { impersonate } = useAuth();
const { teacherId } = props;
const { data, loading } = useAllClassesAndTeacherForInstitution(teacherId!);
const [open, setOpen] = useState(false);
const openDialog = useCallback(() => setOpen(true), []);
const closeDialog = useCallback(() => setOpen(false), []);
const spacing = useThemeSpacing(4)();
const teacherName = `${data?.teacher.name.fullName}`;
const impersonateTeacher = useCallback(() => {
if (!teacherName || !teacherId) {
return;
}
impersonate(teacherId!, 'teacher', teacherName);
closeDialog();
// props?.navigate?.('/');
}, [impersonate, closeDialog, teacherId, teacherName]);
const tabOptions = useMemo(
() => [
{
label: `Clases de ${teacherName}`,
},
{
label: 'Agregar Clases',
},
],
[teacherName],
);
return (
<>
<Typography variant="h1" className={spacing.marginTopBottom}>
{(loading && <Skeleton />) || teacherName}
</Typography>
<Dialog
title={`Entrar en la cuenta de ${teacherName}`}
open={open}
onAgree={impersonateTeacher}
onClose={closeDialog}
>
¿Desea visualizar la cuenta de {teacherName}?
<br />
Si desea salir de la misma por favor refresque la página.
</Dialog>
<Button className={spacing.marginTopBottom} onClick={openDialog}>
Entrar en cuenta de {teacherName || 'maestro'}
</Button>
{process.env.NODE_ENV === 'development' && (
<>
<Tabs options={tabOptions}>
<>
{data?.teacher.klasses && (
<ListClassTeacher
klasses={data.teacher.klasses}
teacherName={teacherName || 'maestro'}
/>
)}
</>
<>
{data?.grades && (
<AddClassTeacher
existingClasses={data?.teacher.klasses || []}
grades={data.grades}
teacherId={teacherId!}
/>
)}
</>
</Tabs>
</>
)}
</>
);
});
export default TeacherView;
Here's the initial Provider:
import React, { Suspense, memo } from 'react';
import { Location } from '#reach/router';
import { ThemeProvider } from '#material-ui/core';
import { ApolloProvider } from '#apollo/client';
import { theme } from 'components';
import { Auth0Provider } from '#auth0/auth0-react';
import CircularLoader from './components/CircularProgress';
import { useGlobalClient } from './utilities/client';
import { Layout } from './views/Layout';
import { Root } from './views/Root';
import { enableIfNotPreRendering } from './utilities/isPrerendering';
import { AUTH_CONFIG } from './auth/auth0.variables';
console.log('AUTH CONFIG', AUTH_CONFIG);
function App() {
// This will be a method to enable faster loading times.
/**\
* Main AppMethod which hosts the site. To improve FCP it was split into
* 2 files: The main file which will load the <Home component without any
* dependencies (making it extremely fast to load at the beginning as it won't)
* download all the code on its entirety.
*
* All consumers that are descendants of a Provider will re-render whenever the Provider’s value prop changes.
* The propagation from Provider to its descendant consumers is not subject to the
* shouldComponentUpdate method, so the consumer is updated even when an ancestor component
* bails out of the update.
*
* Check this out whenever you're planning on implementing offline capabilities:
* https://dev.to/willsamu/how-to-get-aws-appsync-running-with-offline-support-and-react-hooks-678
*/
return (
<Suspense fallback={<CircularLoader scrollsToTop={true} />}>
<Location>
{({ location }) => (
<Auth0Provider
{...AUTH_CONFIG}
location={{ pathname: location.pathname, hash: location.hash }}
>
<ProviderForClient />
</Auth0Provider>
)}
</Location>
</Suspense>
);
}
/**
* This is done like this because we are using the useAuth0 Hook
* and we need it to be after the Auth0Provider!!
* #param props
*/
export const ProviderForClient: React.FC = (props) => {
const globalClient = useGlobalClient();
return (
<ThemeProvider theme={theme}>
<ApolloProvider client={globalClient.current as any}>
<Layout>
<>{enableIfNotPreRendering() && <Root />}</>
</Layout>
</ApolloProvider>
</ThemeProvider>
);
};
export default memo(App);
I feel retarded. The hook was functioning normally. There's nothing wrong with the approach above (some other things can be debated). The problem was that I was not passing the hook via a context, but I was just calling the hook on each of the components. This meant that the hook was recreating the state (as it should be) per component, so whenever I updated the state, it would only be stated in one component.
That was it. I just had to rewrite the hook as a component and share it with a context.
Here's the final code (omitted some typings due to brevity)
const AuthContext = createContext<Context>({
isAuthenticated: false,
isLoading: false,
getAccessTokenSilently() {
return '' as any;
},
getAccessTokenWithPopup() {
return '' as any;
},
getIdTokenClaims() {
return '' as any;
},
loginWithPopup() {
return '' as any;
},
loginWithRedirect() {
return '' as any;
},
logout() {
return '' as any;
},
impersonate(a: string, b: AccountType, c: string) {},
exitImpersonation() {},
setSelectedTenantIndex(i: number) {},
accountType: 'teacher',
employeeId: '',
mode: 'NORMAL',
selectedTenant: {
accountType: 'teacher',
tenantType: 'INSTITUTION',
tenantId: '',
},
tenantId: '',
tenantType: 'INSTITUTION',
user: {
name: '',
nickname: '',
} as any,
variant: 'INSTITUTION_TEACHER',
});
/**
* A custom wrapper for Auth. This allows us to set impersonation
*/
export const Auth: React.FC = memo((props) => {
const { user: _user, isAuthenticated, isLoading, ...auth } = useAuth0();
const user = _user as Auth0HookUser;
const defaultUser = useCallback(
(user?: Auth0HookUser) => getDefaultAuthTenant(user),
[],
);
const [selectedTenantIndex, setSelectedTenantIndex] = useState(0);
const [state, dispatch] = useReducer<Reducer<State, Action>>(reducer, {
current: defaultUser(_user),
original: defaultUser(_user),
});
const calledDispatch = useCallback(dispatch, [dispatch]);
const impersonate = useCallback(
(
impersonatedEmployeeId: string,
accountType: AccountType,
impersonatedName: string,
) => {
if (!user) {
return;
}
calledDispatch({
type: 'IMPERSONATE',
accountType,
impersonatedEmployeeId,
impersonatedName,
});
},
[calledDispatch, user],
);
const exitImpersonation = useCallback(() => {
dispatch({ type: 'EXIT_IMPERSONATION' });
}, []);
useEffect(() => {
if (isLoading || (!isLoading && !isAuthenticated)) {
return;
}
if (!user || state.current.mode === 'IMPERSONATION') {
return;
}
const { tenants, userId } = user['https://app.schon.io/user_data'];
const selectedTenant = tenants[selectedTenantIndex];
const { accountType, tenantType } = selectedTenant;
dispatch({
type: 'UPDATE_AUTH',
auth: {
tenantId: selectedTenant.tenantId,
employeeId: userId,
mode: 'NORMAL',
variant: getVariant(accountType, tenantType),
user,
selectedTenant,
accountType,
tenantType,
},
});
// eslint-disable-next-line
}, [
user,
isAuthenticated,
isLoading,
selectedTenantIndex,
state.current.mode,
]);
return (
<AuthContext.Provider
value={{
isAuthenticated,
isLoading,
impersonate,
exitImpersonation,
setSelectedTenantIndex,
...auth,
...state.current,
}}
>
{props.children}
</AuthContext.Provider>
);
});
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error(
'You need to place the AuthContext below the Auth0Context and on top of the app',
);
}
return context;
}
Now, I just placed the <Auth/> component on the top of my app.
I am trying to build my own exporter function but I need to call a different dataProvider. I don't want to use the fetchRelatedRecords as there could be thousands of ids and I would hit the database way too many times. I am trying to send the filters from my list component to the exporter but I have no idea how to do it. What could be a possible way?
<List
aside={<OrdersAside kpis={kpis} />}
bulkActionButtons={false}
filters={<OrdersFilter />}
sort={{ field: 'pickup_at', order: 'DESC' }}
filterDefaultValues={{
pickup_at__gt: pickupAtGt,
}}
exporter={exporter}
{...props}
>
Many thanks
The exporter function doesn't have access to the filter, as it's already applied to query the list of records that the exporter receives as first parameter.
If you need to do custom queries within the exporter, you should know that the exporter function receives the dataProvider as first argument.
And if you can't do exactly what you want with the <ExportButton>, then replace it with your own component! The implementation isn't super complex:
import * as React from 'react';
import { useCallback, FunctionComponent } from 'react';
import PropTypes from 'prop-types';
import DownloadIcon from '#material-ui/icons/GetApp';
import {
Button,
fetchRelatedRecords,
useDataProvider,
useNotify,
useListContext,
SortPayload,
Exporter,
FilterPayload,
} from 'react-admin';
const ExportButton = props => {
const {
maxResults = 1000,
onClick,
label = 'ra.action.export',
icon = defaultIcon,
exporter: customExporter,
...rest
} = props;
const {
filterValues,
resource,
currentSort,
exporter: exporterFromContext,
total,
} = useListContext(props);
const exporter = customExporter || exporterFromContext;
const dataProvider = useDataProvider();
const notify = useNotify();
const handleClick = useCallback(
event => {
dataProvider
.getList(resource, {
sort: currentSort,
filter: filterValues,
pagination: { page: 1, perPage: maxResults },
})
.then(
({ data }) =>
// here, do what you want with the data
// ...
// the default implementation is:
exporter &&
exporter(
data,
fetchRelatedRecords(dataProvider),
dataProvider,
resource
)
)
.catch(error => {
console.error(error);
notify('ra.notification.http_error', 'warning');
});
if (typeof onClick === 'function') {
onClick(event);
}
},
[
currentSort,
dataProvider,
exporter,
filterValues,
maxResults,
notify,
onClick,
resource,
sort,
]
);
return (
<Button
onClick={handleClick}
label={label}
disabled={total === 0}
{...sanitizeRestProps(rest)}
>
{icon}
</Button>
);
};
const defaultIcon = <DownloadIcon />;
const sanitizeRestProps = ({
basePath,
filterValues,
resource,
...rest
}) =>
rest;
ExportButton.propTypes = {
basePath: PropTypes.string,
exporter: PropTypes.func,
filterValues: PropTypes.object,
label: PropTypes.string,
maxResults: PropTypes.number,
resource: PropTypes.string,
sort: PropTypes.exact({
field: PropTypes.string,
order: PropTypes.string,
}),
icon: PropTypes.element,
};
export default ExportButton;
u need create react-admin <ExportButton filter={{ ...filterValues, ...permanentFilter }} /> in CustomListActions
there are you have access to filterVaues and permanentFilter objects from props o CustomListActions
When a user adds additional information, a mutation is made to the database adding the new info, then the local state is updated, adding the new information to the lead.
My mutation and state seem to get updated fine, the issue seems to be that the state of the Material Table component does not match its 'data' prop. I can see in the React Dev tools that the state was updated in the parent component and is being passes down, the table just seems to be using stale data until I manually refresh the page.
I will attach images of the React Devtools as well as some code snippets. Any help would be much appreciated.
Devtools Material Table data prop:
Devtools Material Table State
Material Table Parent Component:
const Leads = () => {
const [leadState, setLeadState] = useState({});
const [userLeadsLoaded, setUserLeadsLoaded] = React.useState(false);
const [userLeads, setUserLeads] = React.useState([]);
const { isAuthenticated, user, loading } = useAuth()
const [
createLead,
{ data,
// loading: mutationLoading,
error: mutationError },
] = useMutation(GQL_MUTATION_CREATE_LEAD);
const params = { id: isAuthenticated ? user.id : null };
const {
loading: apolloLoading,
error: apolloError,
data: apolloData,
} = useQuery(GQL_QUERY_ALL_LEADS, {
variables: params,
});
useEffect(() => {
if (apolloData) {
if (!userLeadsLoaded) {
const { leads } = apolloData;
const editable = leads.map(o => ({ ...o }));
setUserLeads(editable);
setUserLeadsLoaded(true);
};
}
}, [apolloData])
if (apolloLoading) {
return (
<>
<CircularProgress variant="indeterminate" />
</>
);
};
if (apolloError) {
console.log(apolloError)
//TODO: Do something with the error, ie default user?
return (
<div>
<div>Oh no, there was a problem. Try refreshing the app.</div>
<pre>{apolloError.message}</pre>
</div>
);
};
return (
<>
<Layout leadState={leadState} setLeads={setUserLeads} leads={userLeads} setLeadState={setLeadState} createLead={createLead}>
{apolloLoading ? (<CircularProgress variant="indeterminate" />) : (<LeadsTable leads={userLeads} setLeads={setUserLeads} />)}
</Layout>
</>
)
}
export default Leads
Handle Submit function for adding additional information:
const handleSubmit = async (event) => {
event.preventDefault();
const updatedLead = {
id: leadState.id,
first_name: leadState.firstName,
last_name: leadState.lastName,
email_one: leadState.email,
address_one: leadState.addressOne,
address_two: leadState.addressTwo,
city: leadState.city,
state_abbr: leadState.state,
zip: leadState.zipCode,
phone_cell: leadState.phone,
suffix: suffix,
address_verified: true
}
const { data } = await updateLead({
variables: updatedLead,
refetchQueries: [{ query: GQL_QUERY_GET_USERS_LEADS, variables: { id: user.id } }]
})
const newLeads = updateIndexById(leads, data.updateLead)
console.log('New leads before setLeads: ', newLeads)
setLeads(newLeads)
// setSelectedRow(data.updateLead)
handleClose()
};
Material Table Component:
const columnDetails = [
{ title: 'First Name', field: 'first_name' },
{ title: 'Last Name', field: 'last_name' },
{ title: 'Phone Cell', field: 'phone_cell' },
{ title: 'Email', field: 'email_one' },
{ title: 'Stage', field: 'stage', lookup: { New: 'New', Working: 'Working', Converted: 'Converted' } },
{ title: 'Active', field: 'active', lookup: { Active: 'Active' } },
];
const LeadsTable = ({ leads, setLeads }) => {
const classes = useStyles();
const { user } = useAuth();
const [isLeadDrawerOpen, setIsLeadDrawerOpen] = React.useState(false);
const [selectedRow, setSelectedRow] = React.useState({});
const columns = React.useMemo(() => columnDetails);
const handleClose = () => {
setIsLeadDrawerOpen(!isLeadDrawerOpen);
}
console.log('All leads from leads table render: ', leads)
return (
<>
<MaterialTable
title='Leads'
columns={columns}
data={leads}
icons={tableIcons}
options={{
exportButton: false,
hover: true,
pageSize: 10,
pageSizeOptions: [10, 20, 30, 50, 100],
}}
onRowClick={(event, row) => {
console.log('Selected Row:', row)
setSelectedRow(row);
setIsLeadDrawerOpen(true);
}}
style={{
padding: 20,
}}
/>
<Drawer
variant="temporary"
open={isLeadDrawerOpen}
anchor="right"
onClose={handleClose}
className={classes.drawer}
>
<LeadDrawer onCancel={handleClose} lead={selectedRow} setLeads={setLeads} setSelectedRow={setSelectedRow} leads={leads} />
</Drawer>
</>
);
};
export default LeadsTable;
Try creating an object that contains refetchQueries and awaitRefetchQueries: true. Pass that object to useMutation hook as a 2nd parameter. See example below:
const [
createLead,
{ data,
loading: mutationLoading,
error: mutationError },
] = useMutation(GQL_MUTATION_CREATE_LEAD, {
refetchQueries: [{ query: GQL_QUERY_GET_USERS_LEADS, variables: { id: user.id } }],
awaitRefetchQueries: true,
});
Manually updating cache. Example blow is adding a new todo. In your case you can find and update the record before writing the query.
const updateCache = (cache, {data}) => {
// Fetch the todos from the cache
const existingTodos = cache.readQuery({
query: GET_MY_TODOS
});
// Add the new todo to the cache (or find and update an existing record here)
const newTodo = data.insert_todos.returning[0];
cache.writeQuery({
query: GET_MY_TODOS,
data: {todos: [newTodo, ...existingTodos.todos]}
});
};
const [addTodo] = useMutation(ADD_TODO, {update: updateCache});
I have a post route:
router.post("/projects", async (req, res) => {
const {
projectName,
projectDescription,
projectBudget,
projectDuration,
industry,
companyName,
numberOfEmployees,
diamond,
} = req.body;
console.log(diamond);
const [projectDiamond] = diamond;
const { criteria } = projectDiamond;
//diamond is an array containing an object, and that object contains another object called criteria, hence destructuring the 'criteria' object. It's redundant I know but this thing is out of scope of this question!
if (
!projectName ||
!projectDescription ||
!projectBudget ||
!projectDuration ||
!industry ||
!companyName ||
!numberOfEmployees ||
!diamond
) {
return res.status(422).send({ error: "Must provide all project details" });
}
try {
const project = new Project({
projectName,
projectDescription,
projectBudget,
projectDuration,
industry,
companyName,
numberOfEmployees,
diamond,
userId: req.user._id,
});
const recommendation = await Recommendation.find({
"diamond.criteria": criteria,
}); //Need to render this on screen
const projectsWithSameDiamond = await Project.find({
"diamond.criteria": criteria,
}); //Need to render this on screen
const projectsWithSameIndustry = await Project.find({ industry }); //Need to render this on screen
await project.save();
} catch (err) {
res.status(422).send({ error: err.message });
}
});
It's a post request as you can see. Now every time a user let's say "post a new project", I want to retrieve "recommendation" and "projects with similar diamond" and "projects with similar industry" (you can see that I am trying to save all three of them in different variables in the post route).
Is there a way to retrieve these three variables and use them in a component in react native?
Suppose I have a component, A.js:
const A = () => {
return(
//returning something here
)
)
}
Now suppose this component uses axios to send http post request to the route I have defined:
const A = () => {
...
axios.post("/projects", {projectName,
projectDescription,
projectBudget,
projectDuration,
industry,
companyName,
numberOfEmployees,
diamond} );
return(
//returning something here
)
)
}
After the request has finished successfully ( and a project is posted) I want to let's say render those three variables on screen
const A = () => {
return(
<Text>{recommendation}</Text> {/*Not sure how to do get this after using axios to post new project */}
<Text>{projectWithSimilarDiamond</Text> {/*Not sure how to do get this after using axios to post new project */}
<Text>{projectWithSimilarIndustry}</Text> {/*Not sure how to do get this after using axios to post new project */}
)
)
}
Here is an example using state
snack: https://snack.expo.io/#ashwith00/get-post
code:
import * as React from 'react';
import { Text, View, StyleSheet, ActivityIndicator } from 'react-native';
import Constants from 'expo-constants';
import Axios from 'axios';
// You can import from local files
import AssetExample from './components/AssetExample';
// or any pure javascript modules available in npm
import { Card } from 'react-native-paper';
export default function App() {
const [state, setState] = React.useState({
loading: false,
data: [],
error: false,
});
const getPosts = async () => {
setState({
...state,
loading: true,
data: [],
error: false,
});
// try {
// const response = await Axios.post('/users/post', {
// //datas
// });
// setState({
// ...state,
// loading: false,
// data: response.data,
// });
// } catch (err) {
// setState({
// error: true,
// });
// }
};
React.useEffect(() => {
getPosts()
}, [])
return (
<View style={styles.container}>
{state.loading ? (
<ActivityIndicator />
) : (
state.data.map(({ title }, key) => {
<Text {...{ key }}>{title}</Text>;
})
)}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
paddingTop: Constants.statusBarHeight,
backgroundColor: '#ecf0f1',
padding: 8,
},
paragraph: {
margin: 24,
fontSize: 18,
fontWeight: 'bold',
textAlign: 'center',
},
});
I have a react project using AWS amplify, and user cognito pool, I have the authetication setup like this:
export default withAuthenticator(App, {
signUpConfig: {
signUpFields: [
{
label: 'Company', key: 'company', required: true, type: 'string',
},
],
},
});
And this new custom field for Company works very well, but I'd like to add a select input, the docs doesn't helped much on that, so I'd like to know if it's possible, and if yes, how to do that.
Amplify docs
I know this isn't much help at the moment and may require a lot of work if you already have the components, routing and nav of your application built out, but I read somewhere (can't remember where) that they're switching to a slot-based component which can be incorporated with state handling functionality which you can find here. I'd recommend incorporating this new approach sooner rather than later as it appears Amplify is shifting away from the withAuthenticator HOC.
I'm building in TypeScript, but there is a JS implementation which looks almost identical, here's some examples of both (also seen at bottom of post). Notice the <AmplifyAuthenticator> component, where you can customize your sign-in/sign-up components in the exported App, which is now adopting the naming convention of AuthStateApp such as the following:
const AuthStateApp: React.FunctionComponent = () => {
const [authState, setAuthState] = React.useState<AuthState>();
const [user, setUser] = React.useState<object | undefined>();
React.useEffect(() => {
return onAuthUIStateChange((nextAuthState, authData) => {
setAuthState(nextAuthState);
setUser(authData)
});
}, []);
return authState === AuthState.SignedIn && user ? (
<div className="App">
<div>Hello, {user.username}</div>
<AmplifySignOut />
</div>
) : (
<AmplifyAuthenticator>
<AmplifySignUp
slot="sign-up"
formFields={[
{ type: "email", label: 'Email', placeholder: 'Email', hint: 'Enter Your Email', required: true },
{ type: "password" },
{ type: "email" }
]}
/>
</AmplifyAuthenticator>
);
}
From the documentation here
If you’d like to customize the form fields in the Authenticator Sign In or Sign Up component, you can do so by using the formFields property.
For more details on this customization see the amplify-form-field prop documentation and the internal FormFieldType interface.
For example:
...
<AmplifyAuthenticator usernameAlias="email">
<AmplifySignUp
slot="sign-up"
usernameAlias="email"
formFields={[
{
type: "email",
label: "Custom Email Label",
placeholder: "Custom email placeholder"
inputProps: { required: true, autocomplete: "username" },
},
{
type: "password",
label: "Custom Password Label",
placeholder: "Custom password placeholder",
inputProps: { required: true, autocomplete: "new-password" },
},
{
type: "phone_number",
label: "Custom Phone Label",
placeholder: "Custom phone placeholder",
},
]}
/>
<AmplifySignIn slot="sign-in" usernameAlias="email" />
</AmplifyAuthenticator>
...
Yields:
I hope this helps, I'm still trying to figure out the custom cognito attribute stuff in a sign-up form, which I'm sure is similar to the custom signUp calls such as:
await Auth.signUp({
username: 'someuser', password: 'mycoolpassword',
attributes: {
email: 'someuser#somedomain.com', 'custom:favorite_ice_cream': 'chocolate'
}
})
Examples
JS:
import React from 'react';
import './App.css';
import Amplify from 'aws-amplify';
import { AmplifyAuthenticator, AmplifySignOut } from '#aws-amplify/ui-react';
import { AuthState, onAuthUIStateChange } from '#aws-amplify/ui-components';
import awsconfig from './aws-exports';
Amplify.configure(awsconfig);
const AuthStateApp = () => {
const [authState, setAuthState] = React.useState();
const [user, setUser] = React.useState();
React.useEffect(() => {
return onAuthUIStateChange((nextAuthState, authData) => {
setAuthState(nextAuthState);
setUser(authData)
});
}, []);
return authState === AuthState.SignedIn && user ? (
<div className="App">
<div>Hello, {user.username}</div>
<AmplifySignOut />
</div>
) : (
<AmplifyAuthenticator />
);
}
export default AuthStateApp;
TS:
import React from 'react';
import './App.css';
import Amplify from 'aws-amplify';
import { AmplifyAuthenticator, AmplifySignOut } from '#aws-amplify/ui-react';
import { AuthState, onAuthUIStateChange } from '#aws-amplify/ui-components';
import awsconfig from './aws-exports';
Amplify.configure(awsconfig);
const AuthStateApp: React.FunctionComponent = () => {
const [authState, setAuthState] = React.useState<AuthState>();
const [user, setUser] = React.useState<object | undefined>();
React.useEffect(() => {
return onAuthUIStateChange((nextAuthState, authData) => {
setAuthState(nextAuthState);
setUser(authData)
});
}, []);
return authState === AuthState.SignedIn && user ? (
<div className="App">
<div>Hello, {user.username}</div>
<AmplifySignOut />
</div>
) : (
<AmplifyAuthenticator />
);
}
export default AuthStateApp;
For anyone that is still wondering if this is possible, the new version of Amplify UI now supports a Select Field component that can be tailored into your Authenticator.
import { SelectField } from '#aws-amplify/ui-react';
export const DefaultSelectFieldExample = () => (
<SelectField label="Fruit">
<option value="apple">Apple</option>
<option value="banana">Banana</option>
<option value="orange">Orange</option>
</SelectField>
);
For more, refer to documentation here https://ui.docs.amplify.aws/components/selectfield