My Context:
type Props = {
children: React.ReactNode;
};
interface Context {
postIsDraft: boolean;
setPostIsDraft: Dispatch<SetStateAction<boolean>>;
}
const initialContext: Context = {
postIsDraft: false,
setPostIsDraft: (): void => {},
};
const EditPostContext = createContext<Context>(initialContext);
const EditPostContextProvider = ({ children }: Props) => {
const [postIsDraft, setPostIsDraft] = useState<boolean>(
initialContext.postIsDraft
);
return (
<EditPostContext.Provider
value={{
postIsDraft,
setPostIsDraft,
}}
>
{children}
</EditPostContext.Provider>
);
};
export { EditPostContext, EditPostContextProvider };
I set postIsDraft in the parent:
export const ParentComponent = () => {
{ setPostIsDraft } = useContext(EditPostContext);
// some code
const updatePostStatus = (postStatus: boolean) => {
setPostIsDraft(postStatus);
}
// some code
return(
<EditPostContextProvider>
<ChildComponent />
</EditPostContextProvider>
)
}
Then I need to read the value in the child component:
const { postIsDraft } = useContext(EditPostContext);
Only just starting use context, just not sure what I've done wrong. When I try and read the value from context in the child component, I'm only getting back the initial value, not the set value in the parent component.
Your ParentComponent should be wrapped inside provider so as to use it's value:
<EditPostContextProvider>
<ParentComponent />
</EditPostContextProvider>
Generally we can put the provider in index.js file, and wrap <app /> in it
Related
<MyComponent>
<button>Click me</button>
</MyComponent>
interface MyComponentProps {
children: ???;
}
const MyComponent: FC<MyComponentProps> = ({children}) => {
const string = children.??? //I want the string to be "Click me"
}
I tried so many combinations but I always get undefined, thank you so much for your help.
As others have pointed out, you probably shouldn't be accessing the props of a child, as it is an anti-pattern in React. However, it is possible.
Let's do it right, and write a function to do so safely.
First a few type predicate helpers.
const isElement = (child: React.ReactNode): child is React.ReactElement =>
(child as React.ReactElement)?.props !== undefined;
const isLeaf = (child: React.ReactNode): child is React.ReactText =>
typeof child === 'string' || typeof child === 'number';
Then a recursive function, that recursively maps over children, picking out string and numbers. Because children can be an array, we return an array of strings or numbers.
function extractText(children: React.ReactNode): React.ReactText[] {
return React.Children.toArray(children).reduce<React.ReactText[]>((previous, child) => {
if (isElement(child)) {
return [...previous, ...extractText(child.props.children)];
}
if (isLeaf(child)) {
return [...previous, child];
}
return previous;
}, []);
}
Finally, put it all together.
const MyComponent = ({ children }: { children: React.ReactNode }): JSX.Element => {
const string = extractText(children)[0];
console.log(string); // Click me
return <>{children}</>;
};
export default function App() {
return (
<MyComponent>
<button>Click me</button>
</MyComponent>
);
}
Link to Sandbox
You want to type your children as ReactElement
You will then access props by children.props
<MyComponent>
<button>Click me</button>
</MyComponent>
interface MyComponentProps {
children: ReactElement;
}
const MyComponent: FC<MyComponentProps> = ({children}) => {
const string = children.props.children
}
sandbox with the code:
import { ReactElement } from "react";
import "./styles.css";
interface MyComponentProps {
children: ReactElement;
}
const MyComponent = ({ children }: MyComponentProps) => {
const string = children.props.children;
return <>{string}</>;
};
export default function App() {
return (
<MyComponent>
<button>Click me</button>
</MyComponent>
);
}
I created a little different sandbox
import { ReactElement, ReactNode } from "react";
import "./styles.css";
type ChildProps = {
value: string;
children: ReactNode;
};
const Child = (props: ChildProps) => {
return (
<>
the child component ={props.value} {props.children}
</>
);
};
type ComponentProps = {
children: ReactElement;
};
const Component = ({ children }: ComponentProps) => {
const { value, children: nestedChildren } = children.props;
return (
<>
<p>value = {value}</p>
<p>nested children = {nestedChildren}</p>
<p>children = {children}</p>
</>
);
};
export default function App() {
return (
<div className="App">
<Component>
<Child value="test">some inner text</Child>
</Component>
</div>
);
}
Output:
value = test
nested children = some inner text
children = the child component =test some inner text
I am working on testing a component using react-testing-library. The component has an alert which accepts a prop that comes from context to determine whether the alert is open or not.
const PersonRecord = () => {
const {
personSuccessAlert,
setPersonSuccessAlert,
updatePersonSuccessAlert,
setUpdatePersonSuccessAlert,
} = useContext(PeopleContext);
return (
{personSuccessAlert && (
<div className="person-alert-container">
<Alert
ariaLabel="person-create-success-alert"
icon="success"
open={personSuccessAlert}
/>
</div>
)}
)
}
So the above code uses context to pull the value of personSuccessAlert from PeopleContext. If personSuccessAlert is true the alert will display. My context file is set up as follows:
import React, { createContext, useState } from 'react';
export const PeopleContext = createContext();
const PeopleContextProvider = ({ children }) => {
const [personSuccessAlert, setPersonSuccessAlert] = useState(false);
const [updatePersonSuccessAlert, setUpdatePersonSuccessAlert] = useState(
false,
);
return (
<PeopleContext.Provider
value={{
personSuccessAlert,
updatePersonSuccessAlert,
setPersonSuccessAlert,
setUpdatePersonSuccessAlert,
}}>
{children}
</PeopleContext.Provider>
);
};
export default PeopleContextProvider;
Now I am trying to develop a test which passes personSuccessAlert = true to the PersonRecord component.
Here is what I have been trying:
export function renderWithEmptyPerson(
ui,
{
providerProps,
path = '/',
route = '/',
history = createMemoryHistory({ initialEntries: [route] }),
},
) {
return {
...render(
<MockedProvider mocks={getEmptyPerson} addTypename={false}>
<PeopleContextProvider {...providerProps}>
<Router history={history}>
<Route path={path} component={ui} />
</Router>
</PeopleContextProvider>
</MockedProvider>,
),
};
}
describe('empty person record rendering', () => {
afterEach(cleanup);
test('empty person', async () => {
const providerProps = { value: true };
const { getByText, queryByText, queryByLabelText } = renderWithEmptyPerson(
PersonRecord,
{
providerProps,
route: 'people/6d6ed1f4-8294-44de-9855-2999bdf9e3a7',
path: 'people/:slug',
},
);
expect(getByText('Loading...')).toBeInTheDocument();
});
});
I have tried different variations of const providerProps = { value: true };. Replacing value with personSuccessAlert did not work.
Any advice or help is appreciated.
You are passing providerProps to the PeopleContextProvider, but the PeopleContextProvider is not doing anything with the props. You'll need to actually use those props, for example to set the initial state. You could try something like:
const PeopleContextProvider = ({ children, initialPersonSuccessAlert = false }) => {
const [personSuccessAlert, setPersonSuccessAlert] = useState(initialPersonSuccessAlert);
const [updatePersonSuccessAlert, setUpdatePersonSuccessAlert] = useState(
false,
);
return (
<PeopleContext.Provider
value={{
personSuccessAlert,
updatePersonSuccessAlert,
setPersonSuccessAlert,
setUpdatePersonSuccessAlert,
}}>
{children}
</PeopleContext.Provider>
);
};
This would allow you to set the initial state of personSuccessAlert by passing in a initialPersonSuccessAlert prop. You could update your test like so:
const providerProps = { initialPersonSuccessAlert: true };
Alternatively, if you only wanted to make changes in your test file, you could consider updating the renderWithEmptyPerson function to use PeopleContext.Provider directly instead of the PeopleContextProvider component. That will allow you to set the context value however you like.
In the following code I can access a context at the parent and then it's undefined in a child. It works locally with simple FC setups, but fails downstream in a class component.
const HookDialog = () => {
const { data, setData } = useDialog(1); // I work fine
return (
<DialogHook>
<DialogContent>
<h1>Value: {data}</h1>
</DialogContent>
<Footer>
<FooterButton name="positive">Positive</FooterButton>
</Footer>
</DialogHook>
);
}
export const FooterButton: React.FC<FooterButtonProps> = (
{
children,
name,
className,
...props
}) => {
const dialogHook = useDialog(); // I'm undefined!
return(
<Button {...props} className={cssNames} ...>
{children}
</Button>
);
}
export const DialogProvider = props => {
const [dialog, setDialog] = useState<ReactElement<typeof Dialog>>();
const [data, setData] = useState<any>();
return (
<DialogContextHook.Provider value={{ dialog, setDialog, data, setData }} {...props} >
{props.children}
{dialog}
</DialogContextHook.Provider>
)
}
type CloseEvent = (source: string, data:any) => void;
interface useDialog extends DialogContextHook {
close: (source: string) => void;
}
export const useDialog = (
initialValue?: any,
onClose?: CloseEvent) => {
const context = useContext(DialogContextHook);
if (!context) {
throw new Error('useDialog must be used within a DialogProvider');
}
useEffect(() => {
context.setData(initialValue);
},[])
const close = (source: string) => {
context.setDialog(undefined);
onClose?.(source, context.data);
}
return { ...context, close };
}
<DialogProvider>
<VOITable/>
</DialogProvider>
Update
I recreated FooterButton in the downstream project and the same code works, just not when imported.
I'm trying to type a property of a React component using generics. The problem is when I try to render the component as a render prop through another component, it seems to no longer be able to "infer" the correct type - instead it defaults to unknown and I have to use "as any" to make things work.
I've put together a working example here. Note the comments and "as any"'s: https://stackblitz.com/edit/react-ts-4oyi3d?file=index.tsx
Here's the code:
type TabProps<T> = {
data?: T;
};
type TabsProps<T> = {
children: ({
childrenArr
}: {
childrenArr: React.ReactElement<TabProps<T>>[];
}) => React.ReactElement<TabProps<T>>[];
items: React.ReactElement<TabProps<T>>[];
};
type Data = { hello: string };
const Tab = <T extends unknown>({ data }: TabProps<T>) => <div>...</div>;
const Tabs = <T extends unknown>({ children, items }: TabsProps<T>) => {
const childrenArr = useMemo(() => (Array.isArray(items) ? items : [items]), [
items
]);
// ...
// In real application this is where manipulation of the elements in the childrenArr would take place
// Since this is just an example I'll just do this
const manipulatedchildrenArr = childrenArr.map(child => {
return {
...(child as any),
props: { data: { hello: "world is manipulated" } }
};
});
return (
<div>
<div>Hello</div>
<div>
{typeof children === "function"
? children({ childrenArr: manipulatedchildrenArr })
: children}
</div>
</div>
);
};
const App = () => {
const data: Data = { hello: "World" };
return (
<div className="App">
<Tabs items={[<Tab data={data} />]}>
{({ childrenArr }) =>
childrenArr.map(child => (
// Remove "as any" and it will be type unknown and result in an error
<div>{(child.props as any).data.hello}</div>
))
}
</Tabs>
</div>
);
};
As you can see the type of the data prop is lost.
Now I'm not sure if I went outside the scope of what you were looking for and If I did please let me know and I'll adjust the solution..
Update: I forgot to add code for Single tab.
import React from "react";
import ReactDOM from "react-dom";
export interface ITabProps<T> {
data?: T;
handleProcessData: (data: T) => string;
}
export function Tab<T>(props: ITabProps<T>) {
const data = props.data ? props.handleProcessData(props.data) : "None";
return <div>Hello {data}</div>;
}
export type TabElement<T> = React.ReactElement<ITabProps<T>> | React.ReactElement<ITabProps<T>>[]
export interface ITabsProps<T> {
handleManipulation: (data: T) => T;
children: TabElement<T>
}
export function Tabs<T>(props: ITabsProps<T>) {
const array = [] as TabElement<T>[];
if (Array.isArray(props.children))
props.children.forEach((child) => {
let mChild = <Tab<T> handleProcessData={child.props.handleProcessData} data={props.handleManipulation(child.props.data)} /> as TabElement<T>;
array.push(mChild)
})
else {
let mChild = <Tab<T> handleProcessData={props.children.props.handleProcessData} data={props.handleManipulation(props.children.props.data)} /> as TabElement<T>;
array.push(mChild)
}
return <div>{array.map((item) => (item))}</div>;
}
export type Data = { hello: string };
export function App() {
//B.C. it's generic you going to have to have some form of generic control functions
const handleProcessData = (data: Data) => {
//Here you have to specifiy how this specific data type is processed
return data.hello;
};
const handleManipulation = (data: Data) => {
//here you would have all your manipulation logic
return { hello: data.hello + " is manipulated" };
}
//To Make this easier to use you could nest handleProcessData inside the Tabs component
return (
<div>
<Tabs<Data> handleManipulation={handleManipulation}>
<Tab<Data> handleProcessData={handleProcessData} data={{hello: "world1"}} />
<Tab<Data> handleProcessData={handleProcessData} data={{hello: "world2"}} />
</Tabs>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
Consider the following example:
const useCounter = () => {
const [count, setCount] = useState(0);
return [ count, setCount ];
};
const Shower = () => {
const [ value ] = useCounter();
console.log(value); //stays 0
return value;
}
const Setter = () => {
const [ value, setValue ] = useCounter();
console.log(value); //updates on click
return <button onClick={() => setValue(value+1)}>
Add
</button>
}
const App = () => {
return (
<div className="App">
<Setter />
<Shower />
</div>
);
}
What am I doing wrong? I'd expect that it will use the same state no matter where and how many times it gets used, and if that state updates, it should update every component which uses it I think.
Any suggestions?
That's what react context api try to solve.
const CounterContext = React.createContext({
count: 0,
setCount: () => null
})
const CounterProvider = ({ children }) => {
const [count, setCount] = useState(0);
return (
<CounterContext.Provider value={{
count, setCount
}}>
{children}
</CounterContext.Provider>
)
}
const useCounter = () => {
return React.useContext(CounterContext)
};
useCounter will now provide you the same count and setCount in every component you call it.
To use it:
const Shower = () => {
const { count } = useCounter();
return count;
}
const Setter = () => {
const { count, setCount } = useCounter();
return <button onClick={() => setCount(count+1)}>
Add
</button>
}
const App = () => {
return (
<CounterProvider>
<div className="App">
<Setter />
<Shower />
</div>
</CounterProvider>
);
}
useState returns a pair of value and setter. A piece of data and a way to change it, but everytime you instantiate a new Component a new instace of this pair will be created as well. hooks are a great way to share statetul logic between components, not state itself. Shower get's called and a instance of useCounter is created. Setter gets called and a new instance is created. The structure is the same, the state is not.
To share state between components use props, redux or Context API
When sharing things between functional components, I like to use the pattern below, it is the redux-ish reusable version of Federkun's answer above:
// this component should be an ancestor of component sharing state
// note that it works no matter how deep your subcomponents are in the render tree
class SharedStateContextProvider extends React.Component {
/* static propTypes = {
sharedContext: PropTypes.object,
reducer: PropTypes.func,
initialState: PropTypes.object,
children: PropTypes.oneOfType([
PropTypes.node,
PropTypes.arrayOf(PropTypes.node),
]),
} */
constructor(props) {
super(props);
this.state = {
contextValue: { state: props.initialState, dispatch: this.handleDispatch },
};
}
handleDispatch = (action) => {
const { reducer } = this.props;
const { contextValue: { state: sharedState } } = this.state;
const newState = reducer(sharedState, action);
if (newState !== sharedState) {
this.setState(
() => ({
contextValue: { state: newState, dispatch: this.handleDispatch }
})
);
}
}
render() {
const { sharedContext: Context, children } = this.props;
const { contextValue } = this.state;
return (
<Context.Provider value={contextValue}>
{children}
</Context.Provider>
);
}
}
// the actual shared context
const CounterContext = React.createContext();
// add as much logic as you want here to change the state
// as you would do with redux
function counterReducer(state, action) {
switch(action.type) {
case 'setValue':
return {
...state,
value: action.data
};
default:
return state;
}
}
// counterContext is a prop so the dependency in injected
const Shower = ({ counterContext }) => {
// well known redux-ish interface
const { state, dispatch } = React.useContext(counterContext);
console.log(state.value);
return state.value;
}
// counterContext is a prop so the dependency in injected
const Setter = ({ counterContext }) => {
// well known redux-ish interface
const { state, dispatch } = React.useContext(counterContext);
console.log(state.value); //updates on click
return <button onClick={() => dispatch({ type: 'setValue', data: state.value+1 })}>
Add
</button>
}
// the actual shared state
const initialCounterState = { value: 0 };
const App = () => {
return (
<div className="App">
<SharedStateContextProvider
sharedContext={CounterContext}
reducer={counterReducer}
initialState={initialCounterState}
>
<Setter counterContext={CounterContext} />
<Shower counterContext={CounterContext} />
</SharedStateContextProvider>
</div>
);
}
const root = document.getElementById('root');
ReactDOM.render(<App />, root);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>