简体   繁体   English

如何在 React-Admin 中拥有父/全局资源?

[英]How to have a parent/global Resource in React-Admin?

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我的应用程序的数据库结构从Company级别开始,用户可能是多个公司的一部分,他可以随时切换Companies ,其他所有资源都驻留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.每次用户切换公司,整个应用程序都会发生变化,因为数据发生变化,它就像一个Global Filter ,按companyId,需要首先加载的资源,其他所有依赖于它。

So for example, to get the Projects of a Company , the endpoint is /{companyId}/projects例如,要获取CompanyProjects ,端点是/{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.我尝试这样做的方法是在布局中使用 React 上下文,因为我也需要它来包含边栏。

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它工作得不太好,因为它在启动时查询userCompanies 4 次,所以我正在寻找修复或更优雅的解决方案

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另一个挑战是将当前的 companyId 传递给所有子资源,目前我正在使用 filter 参数,但我不知道如何在 Show/Create/Edit 中执行此操作

companyContext.js 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布局.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应用程序.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: useEffect 多次查询 api:

Your useEffect is querying the api 4 times on startup because you haven't set the dependencies array of your useEffect:您的 useEffect 在启动时查询 api 4 次,因为您尚未设置 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:如何使用上下文 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.例如,上下文将公开changeCompany function。

useReducer or useState: useReducer 或 useState:

In your example you are using a reducer to manage your state.在您的示例中,您使用减速器来管理您的 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.当您有涉及多个子值的复杂 state 逻辑或下一个 state 取决于前一个时,useReducer 通常比 useState 更可取。React Docs反应文档

Your company, loading and error state all depend on the response from the api.您的公司,加载和错误 state 都取决于 api 的响应。 However your states don't depend on each other or on the last state.但是,您的状态不依赖于彼此或最后一个 state。

In this case you can use multiple states instead of a reducer.在这种情况下,您可以使用多个状态而不是减速器。

Relaying current companyId to child ressources:将当前 companyId 中继到子资源:

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.在我的示例中,上下文公开了selectedCompanyData以及选定的公司 ID。

That way, you can directly use selectedCompanyData instead of going through the array to find the data each time.这样,您可以直接使用selectedCompanyData而不是每次都通过数组查找数据。

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 :使用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>
    );
};

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM