Related
I have a new app I'm working on that has a lot of CRUD functionality. A list/grid and then a modal with multiple tabs of content and a save button in the modal header. I'm wondering about patterns to separate state logic from the JSX component so it's easier to read/maintain.
I read about "lifting state", but that makes this item component unruly with the callbacks (and it'll get worse for sure as more detail is added). Is it possible to put the handlers and the state itself in a custom hook and then pull them in where needed, instead of having it at "the closest common ancestor"? For example,
const {company, setCompany, updateCompany, getCompanyByCode, createCompany, onNameChangeHandler, onTitleChangeHandler, etc.} from "./useCompany"
Would all users of this hook have the same view of the data and be able to see and effect updates? I've read about putting data fetching logic in hooks, but what about change handlers for what could be 20 or more fields? I think it's the clutter of the callbacks that bothers me the most.
Here is a shortened version of a component that renders the tabs and data for a specific domain item. Is this a use case for context and/or custom hooks?
import {
getCompanyByCode,
updateCompany,
createCompany,
companyImageUpload,
} from "../../api/companyApi";
const initialState: CompanyItemDetailModel = {
code: "",
name: { en: "", es: "" },
title: { en: "", es: "" },
description: { en: "", es: "" },
displayOrder: 0,
enabled: false,
};
export interface CompanyItemProps {
onDetailsDialogCloseHandler: DetailsCancelHandler;
companyCode: string;
onSaveHandler: () => void;
}
const CompanyItem = (props: CompanyItemProps) => {
const { CompanyCode, onDetailsDialogCloseHandler, onSaveHandler } = props;
const classes = useStyles();
const [tabValue, setTabValue] = useState<Number>(0);
const [isLoading, setIsLoading] = useState(true);
const nameRef = useRef({ en: "", es: "" });
const [Company, setCompany] = useState<CompanyItemDetailModel>(initialState);
const [saveGraphics, setSaveGraphics] = useState(false);
useEffect(() => {
async function getCompany(code: string) {
try {
const payload = await getCompanyByCode(code);
nameRef.current.en = payload.name.en;
setCompany(payload);
} catch (e) {
console.log(e);
} finally {
setIsLoading(false);
}
}
if (CompanyCode.length > 0) {
getCompany(CompanyCode);
}
}, [CompanyCode]);
function handleTabChange(e: React.ChangeEvent<{}>, newValue: number) {
setTabValue(newValue);
}
function detailsDialogSaveHandler() {
const CompanyToSave = {
...Company,
name: JSON.stringify({ ...Company.name }),
description: JSON.stringify({ ...Company.description }),
title: JSON.stringify({ ...Company.title }),
};
if (CompanyCode.length > 0) {
updateCompany(CompanyToSave as any).then((e) => handleSaveComplete());
} else {
createCompany(CompanyToSave as any).then((e) => handleSaveComplete());
}
setSaveGraphics(true);
}
function handleSaveComplete() {
onSaveHandler();
}
function detailsDialogCloseHandler(e: React.MouseEvent) {
onDetailsDialogCloseHandler(e);
}
function handleNameTextChange(e: React.ChangeEvent<HTMLInputElement>) {
setCompany((prevState) => ({
...prevState,
name: {
...prevState.name,
en: e.target.value,
},
}));
}
function handleCompanyCodeTextChange(e: React.ChangeEvent<HTMLInputElement>) {
setCompany((prevState) => ({
...prevState,
code: e.target.value,
}));
}
function handleEnabledCheckboxChange(e: React.ChangeEvent<HTMLInputElement>) {
setCompany((prevState) => ({
...prevState,
enabled: e.target.checked,
}));
}
function handleTitleTextChange(e: React.ChangeEvent<HTMLInputElement>) {
setCompany((prevState) => ({
...prevState,
title: {
...prevState.title,
en: e.target.value,
},
}));
}
return (
<DetailsDialog>
<div className={classes.root}>
<Form>
<ModalHeader
name={nameRef.current}
languageCode={"en"}
onTabChange={handleTabChange}
onCancelHandler={detailsDialogCloseHandler}
onSaveHandler={detailsDialogSaveHandler}
tabTypes={[
DetailsTabTypes.Details,
DetailsTabTypes.Images,
]}
/>
<TabPanel value={tabValue} index={0} dialog={true}>
<CompanyDetailsTab
details={Company}
isEditMode={CompanyCode.length > 1}
languageCode={"en"}
handleNameTextChange={handleNameTextChange}
handleCompanyCodeTextChange={handleCompanyCodeTextChange}
handleEnabledCheckboxChange={handleEnabledCheckboxChange}
handleTitleTextChange={handleTitleTextChange}
handleDescriptionTextChange={handleDescriptionTextChange}
/>
</TabPanel>
<TabPanel value={tabValue} index={1} dialog={true}>
<ImageList />
</TabPanel>
</Form>
</div>
</DetailsDialog>
);
};
export default CompanyItem;
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 pretty new to react and hooks, and I'm struggling with useEffect(). I've watched all the vids and read all the docs and still can't quite wrap my head around the error I'm getting. ("onInput is not a function" when my New Article route loads). onInput points to a callback function in my form-hook.js. Why isn't it registering?
In my input.js component:
import React, { useReducer, useEffect } from 'react';
import { validate } from '../../util/validators';
import './Input.css';
const inputReducer = (state, action) => {
switch (action.type) {
case 'CHANGE':
return {
...state,
value: action.val,
isValid: validate(action.val, action.validators)
};
case 'TOUCH': {
return {
...state,
isTouched: true
}
}
default:
return state;
}
};
const Input = props => {
const [inputState, dispatch] = useReducer(inputReducer, {
value: props.initialValue || '',
isTouched: false,
isValid: props.initialValid || false
});
const { id, onInput } = props;
const { value, isValid } = inputState;
useEffect(() => {
console.log(id);
onInput(id, value, isValid)
}, [id, value, isValid, onInput]);
const changeHandler = event => {
dispatch({
type: 'CHANGE',
val: event.target.value,
validators: props.validators
});
};
const touchHandler = () => {
dispatch({
type: 'TOUCH'
});
};
//if statement to handle if you are updating an article and touch the category.... but it's valid
const element =
props.element === 'input' ? (
<input
id={props.id}
type={props.type}
placeholder={props.placeholder}
onChange={changeHandler}
onBlur={touchHandler}
value={inputState.value}
/>
) : (
<textarea
id={props.id}
rows={props.rows || 3}
onChange={changeHandler}
onBlur={touchHandler}
value={inputState.value}
/>
);
return (
<div
className={`form-control ${!inputState.isValid && inputState.isTouched &&
'form-control--invalid'}`}
>
<label htmlFor={props.id}>{props.label}</label>
{element}
{!inputState.isValid && inputState.isTouched && <p>{props.errorText}</p>}
</div>
);
};
export default Input;
useEffect(() => {onInput points to the onInput prop in NewArticle.js component where users can enter a new article.
import Input from '../../shared/components/FormElements/Input';
import { useForm } from '../../shared/hooks/form-hook';
const NewArticle = () => {
const [formState, inputHandler] = useForm({
title: {
value: '',
isValid: false
}
}, false );
return (
<Input
id="title"
element="input"
type="text"
label="Title"
onInput={inputHandler}
/> );
};
export default NewArticle;
...and then in my form-hook.js inputHandler is a callback. So, onInput points to a callback function through a prop. It was working, registering onInput as a function and then, all of a sudden it was throwing an error. What gives?
import { useCallback, useReducer } from 'react';
const formReducer = (state, action) => {
switch (action.type) {
case 'INPUT_CHANGE':
let formIsValid = true;
for (const inputId in state.inputs) {
if (!state.inputs[inputId]) {
continue;
}
if (inputId === action.inputId) {
formIsValid = formIsValid && action.isValid;
} else {
formIsValid = formIsValid && state.inputs[inputId].isValid;
}
}
return {
...state,
inputs: {
...state.inputs,
[action.inputId]: { value: action.value, isValid: action.isValid }
},
isValid: formIsValid
};
case 'SET_DATA':
return {
inputs: action.inputs,
isValid: action.formIsValid
};
default:
return state;
}
};
export const useForm = (initialInputs, initialFormValidity) => {
const [formState, dispatch] = useReducer(formReducer, {
inputs: initialInputs,
isValid: initialFormValidity
});
const inputHandler = useCallback((id, value, isValid) => {
dispatch({
type: 'INPUT_CHANGE',
value: value,
isValid: isValid,
inputId: id
});
}, []);
const setFormData = useCallback((inputData, formValidity) => {
dispatch({
type: 'SET_DATA',
inputs: inputData,
formIsValid: formValidity
});
}, []);
return [formState, inputHandler, setFormData];
};
Thanks, ya'll.
I can give you some advice on how to restructure your code. This will ultimately solve your problem.
Maintain a single source of truth
The current state of your UI should be stored in a single location.
If the state is shared by multiple components, your best options are to use a reducer passed down by the Context API (redux), or pass down the container component's state as props to the Input component (your current strategy).
This means you should remove the Input component's inputReducer.
The onInput prop should update state in the container component, and then pass down a new inputValue to the Input component.
The DOM input element should call onInput directly instead of as a side effect.
Remove the useEffect call.
Separation of Concerns
Actions should be defined separately from the hook. Traditionally, actions are a function that returns an object which is passed to dispatch.
I am fairly certain that the useCallback calls here are hurting performance more than helping. For example inputHandler can be restructured like so:
const inputChange = (inputId, value, isValid) => ({
type: 'INPUT_CHANGE',
value,
isValid,
inputId
})
export const useForm = (initialInputs, initialFormValidity) => {
const [formState, dispatch] = useReducer(formReducer, {
inputs: initialInputs,
isValid: initialFormValidity,
})
const inputHandler = (id, value, isValid) => dispatch(
inputChange(id, value, isValid)
)
}
Learn how to use debugger or breakpoints in the browser. You would quickly be able to diagnose your issue if you put a breakpoint inside your useEffect call.
React/Redux application goes into an infinite loop on using useEffect with object references..
I am trying render pending todos for my application using useEffect.. and passing the array of todos as the second param in useEffect ..but why is not checking the values of the object ?
Container:
const mapDispatchToProps = dispatch => ({ actions: bindActionCreators(RootActions, dispatch) });
const Home = (props) => {
const { root, actions } = props;
useEffect(() => {
getTodos(actions.loadPendingTodo);
}, [root.data]);
return (
<Segment>
<Error {...root } />
<TodoList { ...root } actions={actions} />
</Segment>
);
};
export default connect(mapStateToProps, mapDispatchToProps)(Home);
Action:
export const loadPendingTodo = () => ({
type: LOAD_PENDING_TODO,
data: todoService.loadPendingTodo(),
});
Reducer:
const initialState = {
initial: true,
data: [{
id: 0,
text: 'temp todo',
dueDate: new Date(),
completedDate: '',
isDeleted: false,
isCompleted: false,
}],
error: false,
isLoading: false,
isEdit: false,
};
export default function root(state = initialState, action) {
switch (action.type) {
case LOAD_PENDING_TODO:
return {
...state,
data: [...action.data],
};
...
default:
return state;
}
}
getTodos Method:
export const getTodos = (loadTodo) => {
try {
loadTodo();
} catch (error) {
console.log(error); // eslint-disable-line
}
};
Service:
export default class TodoAppService {
loadPendingTodo() {
return store.get('todoApp').data.filter(todo => !todo.isCompleted && !todo.isDeleted);
}
Can anyone please help me out how to resolve this issue.. and there is no official documentation for this case too :/
Moreover changing the useEffect to the following works but i want to render on every change
useEffect(() => {
getTodos(actions.loadPendingTodo);
}, []);
Fixed it by removing the loadPedningTodo redux actions in useEffect that was causing it to loop and directly setting the data in function from service..
const Home = (props) => {
const { root, actions } = props;
return (
<Segment>
<Error {...root } />
<TodoList isEdit={root.isEdit} todo={todoService.loadPendingTodo()} actions={actions} />
</Segment>
);
};
thanks :)
I got one container connected to one component. Its a select-suggestion component. The problem is that both my container and component are getting too much repeated logic and i want to solve this maybe creating a configuration file or receiving from props one config.
This is the code:
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { goToPageRequest as goToPageRequestCompetitions } from '../ducks/competitions/index';
import { getSearchParam as getSearchCompetitionsParam, getCompetitionsList } from '../ducks/competitions/selectors';
import { goToPageRequest as goToPageRequestIntermediaries } from '../ducks/intermediaries/index';
import { getSearchParam as getSearchIntermediariesParam, getIntermediariesList } from '../ducks/intermediaries/selectors';
import SelectBox2 from '../components/SelectBox2';
export const COMPETITIONS_CONFIGURATION = {
goToPageRequest: goToPageRequestCompetitions(),
getSearchParam: getSearchCompetitionsParam(),
suggestions: getCompetitionsList()
};
export const INTERMEDIARIES_CONFIGURATION = {
goToPageRequest: goToPageRequestIntermediaries(),
getSearchParam: getSearchIntermediariesParam(),
suggestions: getIntermediariesList()
};
const mapStateToProps = (state, ownProps) => ({
searchString: ownProps.reduxConfiguration.getSearchParam(state),
});
const mapDispatchToProps = (dispatch, ownProps) => ({
dispatchGoToPage: goToPageRequestObj =>
dispatch(ownProps.reduxConfiguration.goToPageRequest(goToPageRequestObj)),
});
const mergeProps = (stateProps, dispatchProps, ownProps) => ({
...ownProps,
search: searchParam => dispatchProps.dispatchGoToPage({
searchParam
}),
...stateProps
});
export default withRouter(connect(mapStateToProps, mapDispatchToProps, mergeProps)(SelectBox2));
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Flex, Box } from 'reflexbox';
import classname from 'classnames';
import styles from './index.scss';
import Input from '../Input';
import { AppButtonRoundSquareGray } from '../AppButton';
import RemovableList from '../RemovableList';
const MIN_VALUE_TO_SEARCH = 5;
const NO_SUGGESTIONS_RESULTS = 'No results found';
class SelectBox extends Component {
/**
* Component setup
* -------------------------------------------------------------------------*/
constructor(props) {
super(props);
this.state = {
displayBox: false,
selection: null,
value: '',
items: [],
suggestions: [],
};
}
/**
* Component lifecycle
* -------------------------------------------------------------------------*/
componentWillMount() {
console.log(this.props);
document.addEventListener('mousedown', this.onClickOutside, false);
if (this.props.suggestionsType){
if (this.props.suggestionsType === 'competition'){
this.state.suggestions = this.props.competitionsSuggestions;
}
if (this.props.suggestionsType === 'intermediaries'){
this.state.suggestions = this.props.intermediariesSuggestions;
}
}
}
componentWillUnmount() {
console.log(this.props);
document.removeEventListener('mousedown', this.onClickOutside, false);
}
componentWillReceiveProps(nextProps){
console.log(this.props);
if (this.props.suggestionsType === 'competition') {
this.state.suggestions = nextProps.competitionsSuggestions;
}
if (this.props.suggestionsType === 'intermediaries') {
this.state.suggestions = nextProps.intermediariesSuggestions;
}
}
/**
* DOM event handlers
* -------------------------------------------------------------------------*/
onButtonClick = (ev) => {
ev.preventDefault();
const itemIncluded = this.state.items.find(item => item.id === this.state.selection);
if (this.state.selection && !itemIncluded) {
const item =
this.state.suggestions.find(suggestion => suggestion.id === this.state.selection);
this.setState({ items: [...this.state.items, item] });
}
};
onChangeList = (items) => {
const adaptedItems = items
.map(item => ({ label: item.name, id: item.itemName }));
this.setState({ items: adaptedItems });
};
onClickOutside = (ev) => {
if (this.wrapperRef && !this.wrapperRef.contains(ev.target)) {
this.setState({ displayBox: false });
}
};
onSuggestionSelected = (ev) => {
this.setState({
displayBox: false,
value: ev.target.textContent,
selection: ev.target.id });
};
onInputChange = (ev) => {
this.generateSuggestions(ev.target.value);
};
onInputFocus = () => {
this.generateSuggestions(this.state.value);
};
/**
* Helper functions
* -------------------------------------------------------------------------*/
setWrapperRef = (node) => {
this.wrapperRef = node;
};
executeSearch = (value) => {
if (this.props.suggestionsType === 'competition'){
this.props.searchCompetitions(value);
}
if (this.props.suggestionsType === 'intermediaries'){
this.props.searchIntermediaries(value);
}
};
generateSuggestions = (value) => {
if (value.length > MIN_VALUE_TO_SEARCH) {
this.executeSearch(value);
this.setState({ displayBox: true, value, selection: '' });
} else {
this.setState({ displayBox: false, value, selection: '' });
}
};
renderDataSuggestions = () => {
const { listId } = this.props;
const displayClass = this.state.displayBox ? 'suggestions-enabled' : 'suggestions-disabled';
return (
<ul
id={listId}
className={classname(styles['custom-box'], styles[displayClass], styles['select-search-box__select'])}
>
{ this.state.suggestions.length !== 0 ?
this.state.suggestions.map(suggestion => (<li
className={classname(styles['select-search-box__suggestion'])}
onClick={this.onSuggestionSelected}
id={suggestion.get(this.props.suggestionsOptions.id)}
key={suggestion.get(this.props.suggestionsOptions.id)}
>
<span>{suggestion.get(this.props.suggestionsOptions.label)}</span>
</li>))
:
<li className={(styles['select-search-box__no-result'])}>
<span>{NO_SUGGESTIONS_RESULTS}</span>
</li>
}
</ul>
);
};
renderRemovableList = () => {
if (this.state.items.length > 0) {
const adaptedList = this.state.items
.map(item => ({ name: item.name, itemName: item.id }));
return (<RemovableList
value={adaptedList}
className={classname(styles['list-box'])}
onChange={this.onChangeList}
uniqueIdentifier="itemName"
/>);
}
return '';
};
render() {
const input = {
onChange: this.onInputChange,
onFocus: this.onInputFocus,
value: this.state.value
};
return (
<Flex className={styles['form-selectBox']}>
<Box w={1}>
<div
ref={this.setWrapperRef}
className={styles['div-container']}
>
<Input
{...this.props}
input={input}
list={this.props.listId}
inputStyle={classname('form-input--bordered', 'form-input--rounded', styles.placeholder)}
/>
{ this.renderDataSuggestions() }
</div>
</Box>
<Box>
<AppButtonRoundSquareGray type="submit" className={styles['add-button']} onClick={this.onButtonClick}>
Add
</AppButtonRoundSquareGray>
</Box>
<Box>
{ this.renderRemovableList() }
</Box>
</Flex>
);
}
}
SelectBox.propTypes = {
items: PropTypes.instanceOf(Array),
placeholder: PropTypes.string,
listId: PropTypes.string,
className: PropTypes.string
};
SelectBox.defaultProps = {
items: [],
placeholder: 'Choose an option...',
listId: null,
className: ''
};
export default SelectBox;
As you see, in many places i am validating the type of suggestions and do something with that. Its suppose to be a reusable component, and this component could accept any kind of type of suggestions. If this grows, if will have very big validations and i don't want that. So i think that i want something similar to this:
const mapStateToProps = (state, ownProps) => ({
searchString: ownProps.reduxConfiguration.getSearchParam(state),
});
const mapDispatchToProps = (dispatch, ownProps) => ({
dispatchGoToPage: goToPageRequestObj =>
dispatch(ownProps.reduxConfiguration.goToPageRequest(goToPageRequestObj)),
});
const mergeProps = (stateProps, dispatchProps, ownProps) => ({
...ownProps,
search: searchParam => dispatchProps.dispatchGoToPage({
searchParam
}),
...stateProps
});
How can i make something similar to that?
Here are a few things to consider:
The purpose of using Redux is to remove state logic from your components.
What you've currently got has Redux providing some state and your component providing some state. This is an anti-pattern (bad):
// State from Redux: (line 22 - 24)
const mapStateToProps = (state, ownProps) => ({
searchString: ownProps.reduxConfiguration.getSearchParam(state),
});
// State from your component: (line 65 - 71)
this.state = {
displayBox: false,
selection: null,
value: '',
items: [],
suggestions: [],
};
If you take another look at your SelectBox component - a lot of what it is doing is selecting state:
// The component is parsing the state and choosing what to render (line 79 - 86)
if (this.props.suggestionsType){
if (this.props.suggestionsType === 'competition'){
this.state.suggestions = this.props.competitionsSuggestions;
}
if (this.props.suggestionsType === 'intermediaries'){
this.state.suggestions = this.props.intermediariesSuggestions;
}
}
Turns out, this is precisely what mapStateToProps() is for. You should move this selection logic to mapStateToProps(). Something like this:
const mapStateToProps = (state) => {
let suggestions = null;
switch (state.suggestionType) {
case 'competition':
suggestions = state.suggestions.competition;
break;
case 'intermediaries':
suggestions = state.suggestions.intermediaries;
break;
default:
break;
}
return {
suggestions
};
};
Every time the state updates (in Redux) it will pass new props to your component. Your component should only be concerned with how to render its part of the state. And this leads me to my next point: When your application state is all being managed by Redux and you don't have state logic in your components, your components can simply be functions (functional components).
const SelectBox3 = ({ suggestions }) => {
const onClick = evt => { console.log('CLICK!'); };
const list = suggestions.map((suggestion, index) => {
return (
<li key={index} onClick={onClick}>suggestion</li>
);
});
return (
<ul>
{list}
</ul>
);
};
Applying these patterns, you get components that are very easy to reason about, and that is a big deal if you want to maintain this code into the future.
Also, by the way, you don't need to use mergeProps() in your example. mapDispatchToProps can just return your search function since connect() will automatically assemble the final props object for you.:
const mapDispatchToProps = (dispatch, ownProps) => ({
// 'search' will be a key on the props object passed to the component!
search: searchParam => {
dispatch(ownProps.reduxConfiguration.goToPageRequest({ searchParam });
// (also, your 'reduxConfiguration' is probably something that belongs in
// the Redux state.)
}
});
I highly recommend giving the Redux docs a good read-through. Dan Abramov (and crew) have done a great job of laying it all out in there and explaining why the patterns are the way they are.
Here's the link: Redux.
Also, look into async actions and redux-thunk for dealing with asynchronous calls (for performing a search on a server, for example).
Finally let me say: you're on the right track. Keep working on it, and soon you will know the joy of writing elegant functional components for your web apps. Good luck!