I am trying to use the forwardRef in typescript React to access a child state. I've followed examples online but many are for javascript. I'm getting compiler errors with this.
This is the simplest version I can come up with and the compiler doesn't like the console log. It says ref.current may be undefined, so I've used some && logic but now it says getMyState() does not exist on type 'never'. I've also looked up that error as I've seen it before but don't understand it in this context.
const Parent = () => {
const ref = useRef();
console.log(ref.current && ref.current.getMyState());
return(
<>
<Child ref={ref}/>
</>
);
}
const Child = forwardRef((props, ref) => {
const [item, setItem] = useState('hello');
useImperativeHandle(ref, () => ({getMyState: () => {return item}}), [item]);
return (
<>
bob
</>
);
})
Note: I've read the React docs, my use case is a valid one for forwardRef.
Why am I getting this error and how do I extract the state from the child?
You need to define type for the ref with the form of functions you expose in the Child component.
Try like below.
type ChildHandle = {
getMyState: () => string | null;
};
const Child = forwardRef((props, ref) => {
const [item, setItem] = useState("hello");
useImperativeHandle(
ref,
() => ({
getMyState: () => {
return item;
}
}),
[item]
);
return <>bob</>;
});
const Parent = () => {
const ref = useRef<ChildHandle>();
console.log(ref.current && ref.current.getMyState());
return (
<>
<Child ref={ref} />
</>
);
};
Example of using the handle of Child within Parent
Related
The following component throws me this error message at runtime when I try to render the table as Input value (editable field) with the additional warning given below.
Warning: Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks. You can only call Hooks at the top level of your React function.
This is my code,
const EditableCell = (initialValue: any) => {
const [value, setValue] = React.useState(initialValue);
const onChange = (e: any) => {
setValue(e.target.value);
};
React.useEffect(() => {
setValue(initialValue);
}, [initialValue]);
return <Input type="text" value={value} onChange={onChange} />;
};
const ParameterTable: React.FC<ParameterTableProps> = () => {
const {
decisionMetadataInput: input,
decisionMetadataOutput: output,
paramList
} = testData.data;
const inMeta = React.useMemo(() => input?.metadata ?? [], [input]);
const outMeta = React.useMemo(() => output?.metadata ?? [], [output]);
const allColumn = React.useMemo(
() =>
[...inMeta, ...outMeta].map((meta) => (
<Column
title={
<>
<span>{meta.field}</span>
<br />
<Typography.Text italic>({meta.type})</Typography.Text>
</>
}
key={meta.field}
dataIndex={["data", meta.field]}
render={(dataIndex) => EditableCell(dataIndex)}
/>
)),
[inMeta, outMeta]
);
const datasource = React.useMemo(
() =>
paramList.map((param) => {
const inParam = param?.paramInput?.param ?? [];
const outParam = param?.paramOutput?.param ?? [];
const data = [...inParam, ...outParam].reduce(
(prev, current) => ({
...prev,
[current.field]: current.value
}),
{}
);
return { data, num: param.paramOrder };
}),
[paramList]
);
return (
<Table dataSource={datasource} rowKey="num" pagination={false}>
<Column title="F" dataIndex="num" />
{allColumn}
</Table>
);
};
This is my codesandbox URL - https://codesandbox.io/s/react-typescript-forked-hl179m?file=/src/ParameterTable.tsx
I suspect this happens because I call the function EditableCell inside the useMemo hook, however I'm not sure how to resolve this. Any help/ suggestion would do, thanks.
I'm trying to make the Antd React Table as editable fields, however when I tried to map the dataIndex as value in a seperate function to render in the column I get this 'Uncaught Error: Rendered fewer hooks than expected. This may be caused by an accidental early return statement in React Hooks'
Instead of calling EditableCell as if it were a plain function, wrap it in React.createElement so that React can see that it's a component (and can therefore have hooks called inside it).
Change to
render={(dataIndex) => <EditableCell initialValue={dataIndex} />}
and
const EditableCell = ({ initialValue }: { initialValue: any }) => {
It would also be a good idea to avoid any, which defeats the purpose of using TypeScript - wherever you have an any, identify the actual type that'll be used there, and use that type instead.
i want to pass the data of text-input from child to parent to submit the dynamic form. when i use useEffect the phone blocked but i don't know why.please can someone help me to solve this problem.thanks to tell me if there are another way to pass the data.
child component
const RenderComponents = ({ sendChildToParent) => {
const [inputsVal, setInputsVal] = useState({});
const handleChange = (name, value) => {
setInputsVal({ ...inputsVal, [name]: value });
};
const senddata = () => {
sendChildToParent(inputsVal);
};
useEffect(senddata);
return (
<>
{getData.length === 0 ? (
<Empty />
) : (
getData.map((item, index) => {
switch (item.type) {
case "TextInput":
return (
<>
<InputText
onChangeText={(text) => handleChange(item.nameC, text)}
ModuleName={item.nameC}
placeholder={item.options.placeholder}
required={item.options.required}
key={index}
/>
</>
);
case "Phone":...
Parent Component
export function TemplateScreen(props) {
const navigation = useNavigation();
const [getData, setData] = React.useState(Mydata);
const [childData, setChildData] = useState([]);
const sendChildToParent = (dataFromChild) => {
setChildData(dataFromChild);
};
//*************************************Child Componenet*************** */
const RenderComponents = () => {
const [userTeam, setUserTeam] = useState({});
[...other code here...];
**********Parent Component*******
return (
<ScrollView>
<RenderComponents />
<Button
title="Submit"
onPress={()=>null}
/>...
The structure of your parent component is fine. The issues are in your child component, in the following lines:
const RenderComponents = ({ sendChildToParent) => {
const [inputsVal, setInputsVal] = useState({});
const handleChange = (name, value) => {
setInputsVal({ ...inputsVal, [name]: value });
};
const senddata = () => {
sendChildToParent(inputsVal);
};
useEffect(senddata);
it's not good practice to duplicate the input value in local state. Pass the value down from the parent component as well as the setter function.
you're not passing a dependency array to your useEffect function, so it runs on every render of the component. This sets off the following chain of events:
the parent renders
the child renders
useEffect runs, setting the value of the state in the parent
the parent re-renders
This is an endless loop and what causes your app to lock.
there's no need to wrap the state setting functions in your own functions unless you are planning to do additional work there later. There's also no need to run those functions in your component lifecycle (useEffect), because they will run when the input changes.
missing bracket in the first line.
You could rewrite the components in the following way:
// parent component
export function TemplateScreen(props) {
const navigation = useNavigation();
const [getData, setData] = React.useState(Mydata);
const [childData, setChildData] = useState({});
return (
<ScrollView>
<RenderComponents childData={childData} setChildData={setChildData} />
...
// child component
const RenderComponents = ({ childData, setChildData }) => {
const handleChange = (name, value) => {
setChildData({ ...childData, [name]: value });
};
return (
...
I have a logic in my code that i don't understand, i can update a state (useState) throught my child components but without the "set" function.
Here is my (simplified) code :
const Main = () =>{
const [mission, setMission] = useState({activity:"", env:""})
const see = () =>{
console.log(mission)
}
return (
<Child1 data={mission} />
<button onClick={see}>TEST</button>
)
}
const Child1 = (props) =>{
const {data} = props
const [mission, setMission] = useState(data)
const handleChange = (value) =>{
mission["activity"] = value
}
return (
<Child2 data={mission} onChange={handleChange} />
)
}
const Child2 = (props) =>{
const {data} = props
const [activity, setActivity] = useState(data.activity)
const handleChange = (e) =>{
setActivity(e.target.value)
props.onChange(e.target.value)
}
return (
<input value={data} onChange={handleChange} />
)
}
I tried in sandbox and it work too, "mission" did update it's value throught all childs without any "setMission".
I'm relativily new to react so i miss something but i don't know what, can someone explain ?
Thank you
You probably want to "lift the state up". Only the Main component should useState. Then pass both the state value and update function to the child component. The child component will call this update function when it updates. This will update the state on the parent properly and re-render.
Using your example:
const Main = () => {
// Only one state at the top level
const [mission, setMission] = useState({ activity: "", env: "" });
const see = () => {
console.log(mission);
};
return (
<>
{/* Pass both the value and state update function to the child */}
<Child1 data={mission} update={setMission} />
<button onClick={see}>TEST</button>
<div>{mission.activity}</div>
</>
);
};
const Child1 = (props) => {
const { data, update } = props;
const handleChange = (e) => {
// This will set parent's state
update({ ...data, activity: e.target.value });
};
// You can follow the similar pattern to the next child
return <Child2 data={data} onChange={handleChange} />;
};
You can see a complete working example in this sandbox.
I am using this react library https://github.com/gregberge/loadable-components to load a Component with Ref to access instance values using useImperativeHandle but ref is always null.
Here is my code
import loadable from '#loadable/component';
export default function ParentComponent(props){
const currentPageRef = useRef();
const[currentPage,setCurrentPage]=useState();
const loadPage= (page= 'home') => {
const CurrentPage = loadable(() => import(`./${page}`));
return (
<CurrentPage
ref={currentPageRef}
/>
);
}
useEffect(() => {
console.log(currentPageRef); //This is always logging current(null);
let pageTitle= currentPageRef.current?.getTitle();
let pageSubTitle= currentPageRef.current?.getSubTitle();
console.log(` Page Title=${pageTitle}`); //This is always coming back as null
console.log(`Page SubTitle=${pageSubTitle}`); //This is also always coming back as null
}, [currentPage]);
return (
<div>
<button onClick={() => {
setCurrentPage(loadPage('youtube));
}}>
LoadPage
</button>
</div>
);
}
Where each of the child components contains a useImperativeHandle to expose instance functions but I can't seem to access any of the functions because currentPageRef is always null
Here is an example of one of the child pages that contains the useImperativeHandle implementation
const YouTubePage= React.forwardRef((props,ref)=>{
const [connected, setConnected] = useState(false);
const getTitle = () => {
return connected ? "Your YouTube Channels" : "YouTube";
}
const getSubTitle = () => {
return connected ? "Publishable content is pushed to only connected channels. You may connect or disconnect channel(s) as appropriate" : "Connect a YouTube account to start posting";
}
useImperativeHandle(ref, () => ({ getTitle, getSubTitle }));
return (<div></div>);
});
Any ideas as to why that might be happening?
Thank you
From your code example your aren't actually rendering the component which you set by the state setter:
export default function ParentComponent(props) {
//...
// Render the page
return (
<>
{currentPage}
<div>...</div>
</>
);
}
i have a question, i'm not sure if possible or not.
Here is a sample code.
I have a component, i want everytime they use my component and a children
it should only accept the specific component.
for e.g:
<tableComponent>
<tableHeaderComponent/>
<tableHeaderComponent/>
<tableHeaderComponent/>
</tableComponent>
but for this type it should not be accepted
<tableComponent>
<th>blah</th>
<th>blah2</th>
<yourowncomponent/>
</tableComponent>
thanks,
ping pong
Assign a displayName to the component you want to allow and check inside the parent component if the children have the allowed displayName
const Good = () => <h1>Good</h1>;
Good.displayName = "Good";
const Bad = () => <h1>Bad</h1>;
const WithGoodOnly = ({ children }) => {
let allowed;
if (Array.isArray(children)) {
allowed = children.every(child => child.type.displayName === "Good");
} else {
allowed = children.type.displayName === "Good";
}
return allowed ? <div>{children}</div> : null;
};
rendered
const App = () => (
<WithGoodOnly>
<Good />
</WithGoodOnly>
);
rendered
const App = () => (
<WithGoodOnly>
<Good />
<Good />
</WithGoodOnly>
);
not rendered
const App = () => (
<WithGoodOnly>
<Good />
<Bad />
</WithGoodOnly>
);