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>
</>
);
}
Related
I am new to react and creating my first react app. not sure why the todo list is not saved even though I have used localStorage set and get methods. I am also getting error about the key in my map method. I can't seen to find any issues on my own with the code.Below is the code of the todo list App
import TodoList from "./TodoList";
import {v4 as uuid} from 'uuid'
function App() {
const [todos,setTodos] = useState([{}]);
const inputRef = useRef();
const LOCAL_STORAGE_KEY = "todoapp"
useEffect(() =>{
const storedTodos = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY))
if(storedTodos){
setTodos(storedTodos)}
}, [])
useEffect(() => {
localStorage.setItem(LOCAL_STORAGE_KEY,JSON.stringify(todos))
}, [todos])
function toggleTodo(id){
const newTodos= [...todos]
const todo = newTodos.find(todo => todo.id === id)
todo.complete = !todo.complete
setTodos(newTodos)
}
function handleAdd(e) {
const name = inputRef.current.value;
if(name === "")return
setTodos(prevTodos => {
return [...prevTodos,{id:uuid(),name:name,complete:false}]
})
inputRef.current.value = null;
}
function handleClearTodos(){
const newTodos = todos.filter(todo=>!todo.complete)
setTodos(newTodos)
}
return (
<>
<h1>Chores!!</h1>
<TodoList todo={todos} toggleTodo ={toggleTodo} />
<input ref={inputRef} type="text" />
<button onClick ={handleAdd}>Add todo</button>
<button onClick={handleClearTodos}>Clear todo </button>
<div> {todos.filter(todo => !todo.complete).length} left todo</div>
</>
)
}
export default App;
import Todo from './Todo'
export default function TodoList({todo,toggleTodo}) {
return (
todo.map((todo)=> {
return <Todo key={todo.id} todo={todo} toggleTodo={toggleTodo} />
})
)
}
This:
useEffect(() => {
localStorage.setItem(LOCAL_STORAGE_KEY,JSON.stringify(todos))
}, [todos])
Is probably taking the initial state of todos on the first render (empty array) and overwriting what data was in their with that initial state.
You might think the previous effect counters this since todos is populated from local storage -- but it doesn't, because on that initial render pass, the second effect will only see the old value of todos. This seems counter-intuitive at first. But it's because whenever you call a set state operation, it doesn't actual change the value of todos immediately, it waits until the render passes, and then it changes for the next render. I.e. it is, in a way, "queued".
For the local storage setItem, you probably want to do it in the event handler of what manipulates the todos and not in an effect. See the React docs.
import TodoList from "./TodoList";
import {v4 as uuid} from 'uuid'
function App() {
const [todos,setTodos] = useState([{}]);
const inputRef = useRef();
const LOCAL_STORAGE_KEY = "todoapp"
const storeTodos = (todos) => {
localStorage.setItem(LOCAL_STORAGE_KEY,JSON.stringify(todos))
setTodos(todos)
}
useEffect(() =>{
const storedTodos = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY))
if(storedTodos){
setTodos(storedTodos)}
}, [])
function toggleTodo(id){
const newTodos= [...todos]
const todo = newTodos.find(todo => todo.id === id)
todo.complete = !todo.complete
storeTodos(newTodos)
}
function handleAdd(e) {
const name = inputRef.current.value;
if(name === "")return
storeTodos(prevTodos => {
return [...prevTodos,{id:uuid(),name:name,complete:false}]
})
inputRef.current.value = null;
}
function handleClearTodos(){
const newTodos = todos.filter(todo=>!todo.complete)
storeTodos(newTodos)
}
return (
<>
<h1>Chores!!</h1>
<TodoList todo={todos} toggleTodo ={toggleTodo} />
<input ref={inputRef} type="text" />
<button onClick ={handleAdd}>Add todo</button>
<button onClick={handleClearTodos}>Clear todo </button>
<div> {todos.filter(todo => !todo.complete).length} left todo</div>
</>
)
}
export default App;
As the for the key error, we'd need to see the code in TodoList, but you need to ensure when you map over them, that the id property of each todo is passed to a key prop on the top most element/component within the map callback.
How do you pass a callback function to a sibling component such that a button in one can trigger an action in the another? The following code is an example of an attempt I made that ended up infinitely resetting the state in Index.js
import React, {useState} from "react"
const ComplexComponent = ({setCallbackFunction}) => {
setCallbackFunction(() => console.log("hello world"))
return <div/>
}
const Button = ({onClick}) => {
return (
<button onClick={onClick}>
Submit
</button>
)
}
export default function Index() {
const [callbackFunction, setCallbackFunction] = useState(() => {})
// EXPECTED: callbackFunction should log "hello world" after clicking Button
// ACTUAL: state is reset infinitely
return (
<>
<ComplexComponent setCallbackFunction={setCallbackFunction} />
<Button onClick={callbackFunction}/>
</>
)
}
In this case function is getting executed as soon as you initialize. example below, the function is not returning anything. its just printing initial immediately. Since its not returning anything, undefined will be set to callbackFunction variable.
const [callbackFunction, setCallbackFunction] = React.useState(() =>
console.log('initial')
);
solution:
wrap the function with another function. so that, the outer function returns inner function instead of undefined
const [callbackFunction, setCallbackFunction] = React.useState(
() => () => console.log('initial')
);
now you can reset call back function, again with a wrapper function. without the wrapper same thing here as before. function will get executed immediately and print hello world
const ComplexComponent = ({ setCallbackFunction }) => {
setCallbackFunction(() => () => console.log('hello world'));
return <div />;
};
to avoid different component render error consider useEffect.
const ComplexComponent = ({ setCallbackFunction }) => {
React.useEffect(() => {
setCallbackFunction(() => () => console.log('hello world'));
}, []);
return <div />;
};
I have a parent component that is fetching data from an api, I would like to fetch it once on render and set it to state and pass it down as props to a child component that will then map over this state. How can I wait for 'bids' to have items before the child component renders? I thought by passing setBids it would wait until bids is set, but instead im getting an empty array to map over
function Parent () {
const [bids, setBids] = useState([]);
useEffect(() => {
const fetchBids = async () => {
const result = await api.GetBids();
setBids(result.body);
};
fetchBids();
}, [ setBids ]);
return ( <Child bids={bids}/>)
}
export default Parent;
function Child(props) {
const { bids, id } = props;
return (
<Fragment>
{bids.map((responseBidId) => {
if (responseBidId === id) {
return (
<button onClick={handleView}>view</button>
);
} else {
return (
<Button
onClick={handleUpload}
>
Upload
</Button>
);
}
})}
</Fragment>
);
}
export default Child;
You could wait for bids to be populated before rendering the child:
const [bids, setBids] = useState();
return bids ? <Child bids={bids}/> : null;
You can't "wait", not really. If you think about it more in terms of "when" should I render the child it might help though.
function Parent () {
const [bids, setBids] = useState(null); // <-- change to null to indicate unset
useEffect(() => {
const fetchBids = async () => {
const result = await api.GetBids();
setBids(result.body);
};
fetchBids();
}, [ setBids ]);
return bids ? <Child bids={bids}/> : null;
}
export default Parent;
Playing with React those days. I know that calling setState in async. But setting an initial value like that :
const [data, setData] = useState(mapData(props.data))
should'nt it be updated directly ?
Bellow a codesandbox to illustrate my current issue and here the code :
import React, { useState } from "react";
const data = [{ id: "LION", label: "Lion" }, { id: "MOUSE", label: "Mouse" }];
const mapData = updatedData => {
const mappedData = {};
updatedData.forEach(element => (mappedData[element.id] = element));
return mappedData;
};
const ChildComponent = ({ dataProp }) => {
const [mappedData, setMappedData] = useState(mapData(dataProp));
console.log("** Render Child Component **");
return Object.values(mappedData).map(element => (
<span key={element.id}>{element.label}</span>
));
};
export default function App() {
const [loadedData, setLoadedData] = useState(data);
const [filter, setFilter] = useState("");
const filterData = () => {
return loadedData.filter(element =>
filter ? element.id === filter : true
);
};
//loaded comes from a useEffect http call but for easier understanding I removed it
return (
<div className="App">
<button onClick={() => setFilter("LION")}>change filter state</button>
<ChildComponent dataProp={filterData()} />
</div>
);
}
So in my understanding, when I click on the button I call setFilter so App should rerender and so ChildComponent with the new filtered data.
I could see it is re-rendering and mapData(updatedData) returns the correct filtered data BUT ChildComponent keeps the old state data.
Why is that ? Also for some reason it's rerendering two times ?
I know that I could make use of useEffect(() => setMappedData(mapData(dataProp)), [dataProp]) but I would like to understand what's happening here.
EDIT: I simplified a lot the code, but mappedData in ChildComponent must be in the state because it is updated at some point by users actions in my real use case
https://codesandbox.io/s/beautiful-mestorf-kpe8c?file=/src/App.js
The useState hook gets its argument on the very first initialization. So when the function is called again, the hook yields always the original set.
By the way, you do not need a state there:
const ChildComponent = ({ dataProp }) => {
//const [mappedData, setMappedData] = useState(mapData(dataProp));
const mappedData = mapData(dataProp);
console.log("** Render Child Component **");
return Object.values(mappedData).map(element => (
<span key={element.id}>{element.label}</span>
));
};
EDIT: this is a modified version in order to keep the useState you said to need. I don't like this code so much, though! :(
const ChildComponent = ({ dataProp }) => {
const [mappedData, setMappedData] = useState(mapData(dataProp));
let actualMappedData = mappedData;
useMemo(() => {
actualMappedData =mapData(dataProp);
},
[dataProp]
)
console.log("** Render Child Component **");
return Object.values(actualMappedData).map(element => (
<span key={element.id}>{element.label}</span>
));
};
Your child component is storing the mappedData in state but it never get changed.
you could just use a regular variable instead of using state here:
const ChildComponent = ({ dataProp }) => {
const mappedData = mapData(dataProp);
return Object.values(mappedData).map(element => (
<span key={element.id}>{element.label}</span>
));
};
My component roughly looks like this:
import useCustomHook from "./hooks";
const Test = () => {
const [data, setData] = useCustomHook("example-key", {ready: false});
return (
data.ready ?
<>
{console.log("data is ready");}
<ComponentA />
</> :
<>
{console.log("data is not ready");}
<ComponentB />
</>
)
}
useCustomHook is helper hook that pulls from AsyncStorage for my native application, so it has a slight delay. When I run this, I see the console logs "data is not ready" followed by "data is ready", but I only see ComponentB render, and never ComponentA.
If it's helpful, the custom hook looks like this. It basically just serializes the JSON into a string for storage.
export default (key, initialValue) => {
const [storedValue, setStoredValue] = React.useState(initialValue);
React.useEffect(() => {
const populateStoredValue = async () => {
const storedData = await AsyncStorage.getItem(key);
if (storedData !== null) {
setStoredValue(JSON.parse(storedData))
}
}
populateStoredValue()
}, [initialValue, key]);
const setValue = async (value) => {
const valueToStore = value instanceof Function ? value(storedValue) : value;
await AsyncStorage.setItem(key, JSON.stringify(valueToStore));
setStoredValue(valueToStore);
}
return [storedValue, setValue];
}
Anyone have ideas on what might be happening here?
Thanks!
small PS: I also see the warning Sending "onAnimatedValueUpdate" with no listeners registered., which seems like it's a react-navigation thing that's unrelated. But just wanted to put that here.
First of all, as your key param in custom hook is undefined so data will never be set. Pass the key to the custom hook as the prop.
Secondly, you need to update your condition to check if data is present or not, assuming ready property exist on data after set, like this:
import useCustomHook from "./hooks";
const Test = () => {
const [data, setData] = useCustomHook(/* Add key here */);
return (
data && data.ready ?
<>
console.log("data is ready");
<ComponentA />
</> :
<>
console.log("data is not ready");
<ComponentB />
</>
)
}