Context:
The database structure of my application starts at the Company
level, a user may be part of more than one company, and he may switch Companies
at any point, every other Resource resides inside Company
Every time the user switches company, the whole application changes, because the data changes, it's like a Global Filter, by companyId, a resource that needs to be loaded first and all other depend on it.
So for example, to get the Projects
of a Company
, the endpoint is /{companyId}/projects
The way I've tried to do this is using a React Context in the Layout, because I need it to encompass the Sidebar as well.
It's not working too well because it is querying userCompanies
4 times on startup, so I'm looking either for a fix or a more elegant solution
Another challenge is also to relay the current companyId to all child Resources, for now I'm using the filter parameter, but I don't know how I will do it in Show/Create/Edit
companyContext.js
const CompanyContext = createContext()
const companyReducer = (state, update) => {
return {...state, ...update}
}
const CompanyContextProvider = (props) => {
const dataProvider = useDataProvider();
const [state, dispatch] = useReducer(companyReducer, {
loading : true,
companies : [],
firstLoad : false
})
useEffect(() => {
if(!state.firstLoad) { //If I remove this it goes on a infinite loop
//This is being called 3 times on startup
console.log('querying user companies')
dataProvider.getList('userCompanies')
.then(({data}) =>{
dispatch({
companies: data,
selected: data[0].id, //Selecting first as default
loading: false,
firstLoad: true
})
})
.catch(error => {
dispatch({
error: error,
loading: false,
firstLoad: true
})
})
}
})
return (
<CompanyContext.Provider value={[state, dispatch]}>
{props.children}
</CompanyContext.Provider>
)
}
const useCompanyContext = () => {
const context = useContext(CompanyContext);
return context
}
layout.js
const CompanySelect = ({companies, loading, selected, callback}) => {
const changeCompany = (companyId) => callback(companyId)
if (loading) return <div>Loading...</div>
if (!companies || companies.length < 1) return <div>You are not part of a company</div>
return (
<select value={selected} onChange={(evt) => changeCompany(evt.target.value)}>
{companies.map(company => <option value={company.id} key={company.id}>{company.name}</option>)}
</select>
)
}
const CompanySidebar = (props) => {
const [companyContext, dispatch] = useCompanyContext();
const {companies, selected, loading, error} = companyContext;
const changeCompany = (companyId) => {
dispatch({
selected : companyId
})
}
return (
<div>
<CompanySelect companies={companies} selected={selected} loading={loading} callback={changeCompany}/>
<Sidebar {...props}>
{props.children}
</Sidebar>
</div>
)
}
export const MyLayout = (props) => {
return (
<CompanyContextProvider>
<Layout {...props} sidebar={CompanySidebar}/>
</CompanyContextProvider>
)
};
app.js
const ProjectList = (props) => {
const [companyContext, dispatch] = useCompanyContext();
const {selected, loading} = companyContext;
if(loading) return <Loading />;
//The filter is how I'm passing the companyId
return (
<List {...props} filter={{companyId: selected}}>
<Datagrid rowClick="show">
<TextField sortable={false} source="name" />
<DateField sortable={false} source="createdAt" />
</Datagrid>
</List>
);
};
const Admin = () => {
return (
<Admin
authProvider={authProvider}
dataProvider={dataProvider}
layout={MyLayout}
>
<Resource name="projects" list={ProjectList}/>
</Admin>
);
};
useEffect querying the api multiple times:
Your useEffect is querying the api 4 times on startup because you haven't set the dependencies array of your useEffect:
If you want to run an effect and clean it up only once (on mount and unmount), you can pass an empty array ([]) as a second argument. React Docs
How to use the context api:
In your example you are dispatching to the context reducer from outside of the context.
However, usually, you want to keep all the context logic inside the context extracting only the functions you need to modify the context. That way you can define the functions only once and have less code duplication.
For example the context will expose the changeCompany
function.
useReducer or useState:
In your example you are using a reducer to manage your state.
useReducer is usually preferable to useState when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one. React Docs
Your company, loading and error state all depend on the response from the api. However your states don't depend on each other or on the last state.
In this case you can use multiple states instead of a reducer.
Relaying current companyId to child ressources:
Instead of using a filter in your components, you can directly filter the data you will need in your context. That way you will only have to filter the data once which will optimize performance.
In my example the context exposes selectedCompanyData
as well as the selected company id.
That way, you can directly use selectedCompanyData
instead of going through the array to find the data each time.
Context solution:
const CompanyContext = React.createContext({
companies: [],
selected: null,
loading: true,
changeCompany: (companyId) => {},
updateCompanyData: () => {},
});
const CompanyContextProvider = (props) => {
const { children } = props;
const [selectedCompanyId, setSelectedCompany] = useState(null);
const [companies, setCompanies] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
/* We want useEffect to run only on componentDidMount so
we add an empty dependency array */
useEffect(() => {
dataProvider
.getList('userCompanies')
.then(({ data }) => {
setCompanies(data);
setSelectedCompany(data[0].id);
setIsLoading(false);
setError(null);
})
.catch((error) => {
setIsLoading(false);
setError(error);
});
}, []);
/* You will need to check the guards since I don't know
if you want the id as a number or a string */
const changeCompany = (companyId) => {
if (typeof companyId !== 'number') return;
if (companyId <= 0) return;
setSelectedCompany(companyId);
};
const updateCompanyData = () => {
setIsLoading(true);
dataProvider
.getList('userCompanies')
.then(({ data }) => {
setCompanies(data);
setIsLoading(false);
setError(null);
})
.catch((error) => {
setIsLoading(false);
setError(error);
});
};
/* You will need to check this since I don't know if your api
responds with the id as a number or a string */
const selectedCompanyData = companies.find(
(company) => company.id === selectedCompanyId
);
const companyContext = {
companies,
selected: selectedCompanyId,
loading: isLoading,
error,
selectedCompanyData,
changeCompany,
updateCompanyData,
};
return (
<CompanyContext.Provider value={companyContext}>
{children}
</CompanyContext.Provider>
);
};
const useCompanyContext = () => {
return useContext(CompanyContext);
};
export default CompanyContext;
export { CompanyContext, CompanyContextProvider, useCompanyContext };
Use example with CompanySelect
:
const CompanySelect = () => {
const { changeCompany, loading, companies, selected } = useCompanyContext();
if (loading) return <div>Loading...</div>;
if (!companies || companies.length < 1)
return <div>You are not part of a company</div>;
return (
<select
value={selected}
onChange={(evt) => changeCompany(parseInt(evt.target.value))}
>
{companies.map((company) => (
<option value={company.id} key={company.id}>
{company.name}
</option>
))}
</select>
);
};
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With