at the moment our react components look something like this:
Parent.tsx:
const Child1 = () => ...
const Child2 = () => ...
export const Parent = () => ...
Wrapper.tsx
import { Parent } from 'Parent.tsx'
/// use parent
This makes it difficult to see what the component is actually about since there are multiple in one file - however we also don't want to unnecessarily expose components that are not needed anywhere else. We would like to move the child components to a subfolder and only expose them to their parent.
So in the end the preferred folder structure would be something like this:
src
├── feature
| ├── components
│ | ├── Child1.tsx
│ | └── Child2.tsx
| └── Parent.tsx
└── screens
└── App.tsx <-- imports Parent.tsx - can't import children
Is this somehow possible without exposing them to the whole application? How do you deal with such bigger components in your production applications?
CodePudding user response:
You can't make components that are importable only by other specific components, but you can establish a convention, like your example preferred folder structure, where you just know that anything in a component subfolder is intended for use only within that directory.
A common convention is to have an index file that only exports what's intended for outside use.
src
├── feature
| └── Parent
| ├── index.tsx // re-exports just the Parent.tsx component
| └── components
| ├── Parent.tsx // imports Child1, Child2
| ├── Child1.tsx
| └── Child2.tsx
└── screens
└── App.tsx <-- import Parent from './feature/Parent'
CodePudding user response:
Your app is yours, so why would you care to have this restriction? This has nothing to do directly with React, and so there is little you can do about this.
On the other hand, if this is really a concern to your design usage and you need to create some development restriction, then you can use React for this, creating this restriction.
For example:
// ./FooParent.tsx
import React, { createContext, useContext, useMemo } from 'react';
interface FooRestriction {
checkParent(): void
}
const FooContext = createContext<FooRestriction>({
checkParent() { throw new Error("Cannot use child without it's parent!"); }
});
type FooProps = {
children:React.ReactNode
}
export default function FooParent({ children }:FooProps) {
const contextValue = useMemo<FooRestriction>(() => ({
checkParent() { /* override */ }
}), []);
return (
<FooContext.Provider value={ contextValue }>
{ children }
</FooContext.Provider>
);
}
export function useFooParent() { useContext(FooContext).checkParent(); }
then
// ./children/FooChild.tsx
import React from 'react';
import { useFooParent } from '../FooParent';
export default function FooChild() {
useFooParent();
return (
<div>Child!!</div>
)
}
Now, if someone uses FooChild
without being in a FooParent
, it will throw Cannot use child without it's parent!
Unfortunately, you cannot check if FooChild
is immediately a child of FooParent
-- a minor update to the provided solution above would enable this, though -- , but the question does not make this precision.
It's not a good design, but it does provide you with this restriction.
Edit: I was actually curious how I would use this, so I wrote this (mostly untested) code:
// ./parent-with-immediate-children.tsx
import React, {
createContext,
forwardRef,
useContext,
useEffect,
useImperativeHandle,
useMemo,
useRef
} from 'react';
type ImmediateChildRegistrationCleanup = () => void
type ImmediateChildrenContextType = {
getRegisteredChildren():HTMLElement[]
registerChild(child:HTMLElement):ImmediateChildRegistrationCleanup
}
type ImmediateChildrenProviderProps = {
/**
* Optionally set the maximum number of children this parent can have
*/
maxChildren?:number|undefined
/**
* The ref to the parent HTML element
*/
parentRef:React.MutableRefObject<HTMLElement>
children:React.ReactNode
}
export type ParentImmediateChildrenInfo = {
/**
* Returns all children using the useImmediateChildRef hook
*/
immediateChildren():HTMLElement[]
}
const ImmediateChildrenContext = createContext<ImmediateChildrenContextType>({
getRegisteredChildren() { return []; },
registerChild() { throw new Error("Cannot use child without it's parent!"); }
});
export const ParentWithImmediateChildrenProvider = forwardRef(function ParentWithImmediateChildrenProvider({
maxChildren = 0,
parentRef,
children
}:ImmediateChildrenProviderProps, ref:any) {
const contextValue = useMemo<ImmediateChildrenContextType>(() => {
const registeredChildren = new Set<HTMLElement>();
return {
getRegisteredChildren() {
return Array.from(registeredChildren);
},
registerChild(child:HTMLElement) {
if (!child) {
throw new Error('Child ref not set to an HTML element');
} else if (!parentRef.current) {
throw new Error('Parent ref not set to an HTML element');
} else if (child !== parentRef.current) {
throw new Error('Child has invalid immeidate parent!');
} else if (maxChildren > 0 && (parentRef.current.children.length >= maxChildren)) {
throw new Error('Limit of children reached for parent!');
}
registeredChildren.add(child);
return () => registeredChildren.delete(child);
}
};
}, []);
useImperativeHandle(ref, () => ({
immediateChildren() { return contextValue.getRegisteredChildren(); }
}), []);
return (
<ImmediateChildrenContext.Provider value={ contextValue }>
{children}
</ImmediateChildrenContext.Provider>
);
});
export function useImmediateChildRef<E extends HTMLElement>() {
const childRef = useRef<E>();
const { registerChild } = useContext(ImmediateChildrenContext);
useEffect(() => registerChild(childRef.current), []);
return childRef;
}
Usage:
// ./FooParent.tsx
import React, { useEffect, useRef } from 'react';
import type { ParentImmediateChildrenInfo } from './parent-with-immediate-children';
import { ParentWithImmediateChildrenProvider } from './parent-with-immediate-children';
type FooProps = {
children:React.ReactNode
}
export default function FooParent({ children }:FooProps) {
const registeredChildrenRef = useRef<ParentImmediateChildrenInfo>();
const parentRef = useRef<HTMLDivElement>();
useEffect(() => {
console.log("*** registered children", registeredChildrenRef.current.immediateChildren() );
}, []);
return (
<ParentWithImmediateChildrenProvider ref={ registeredChildrenRef } parentRef={ parentRef }>
<div ref={ parentRef }>
{ children }
</div>
</ParentWithImmediateChildrenProvider>
);
}
and
// ./children/FooChild.tsx
import React from 'react';
import { useImmediateChildRef } from '../parent-with-immediate-children';
export default function FooChild() {
const childRef = useImmediateChildRef<HTMLDivElement>();
return (
<div ref={ childRef }>
Child!!
</div>
)
}