I have an iframe element inside my component and I need to somehow detect when the URL of that iframe changes. What I have tried so far:
Added onLoad callback, but this does not trigger every time I redirect in the iframe
Used React.useCallback but also does not work as I wanted to
Create timeout and get the URL from the iframe every x seconds
Minimalistic code example below
export const XComponent = (props: XComponentProps) => {
const ref = React.useRef<any>();
1.
const onLoad = () => {
const url = ref.current.contentWindow.location.href;
// do stg with url
}
2.
const getRef = React.useCallback((node: any) => {
// store node into state, this was not triggered properly either
}, []);
3.
React.useEffect(() => {
const interval = setInterval(() => {
const url = ref.current.contentWindow.location.href;
}, 1000);
return () => clearInterval(interval);
}, []);
return (
<div className={ styles.albertStoremanTab }>
<div className={ styles.container }>
<iframe id={"iframe-my-id"} onLoad={onLoad} src={props.defaultUrl} ref={ref}></iframe>
</div>
</div>
);
};
That's simple:
useEffect(() => {
// It runs only when defaultUrl changes
}, [props.defaultUrl])
Did you try using ref.current in the dependency array of the useEffect?
useEffect(() => { setURL(ref.current.contentWindow.location.href) },
[ref.current])
Related
There is counter on page. To avoid re-rendering the entire Parent component every second, the counter is placed in a separate Child component.
From time to time there is need to take current time from counter Child (in this example by clicking Button).
I found solution with execution useEffect by passing empty object as dependency.
Even though it works, I don't feel this solution is correct one.
Do you have suggestion how this code could be improved?
Parent component:
const Parent = () => {
const [getChildValue, setGetChildValue] = useState(0);
const [triggerChild, setTriggerChild] = useState(0); // set just to force triggering in Child
const fooJustToTriggerChildAction = () => {
setTriggerChild({}); // set new empty object to force useEffect in child
};
const handleValueFromChild = (timeFromChild) => {
console.log('Current time from child:', timeFromChild);
};
return (
<>
<Child
handleValueFromChild={handleValueFromChild}
triggerChild={triggerChild}
/>
<Button onPress={fooJustToTriggerChildAction} >
Click to take time
</Button>
</>
);
};
Child component
const Child = ({
triggerChild,
handleValueFromChild,
}) => {
const [totalTime, setTotalTime] = useState(0);
const totalTimeRef = useRef(totalTime); // useRef to handle totalTime inside useEffect
const counter = () => {
totalTimeRef.current = totalTimeRef.current + 1;
setTotalTime(totalTimeRef.current);
setTimeout(counter, 1000);
};
useEffect(() => {
counter();
}, []); // Run time counter at first render
useEffect(() => {
const valueForParent = totalTimeRef.current;
handleValueFromChild(valueForParent); // use Parent's function to pass new time
}, [triggerChild]); // Force triggering with empty object
return (
<>
<div>Total time: {totalTime}</div>
</>
);
};
Given your set of requirements, I would do something similar, albeit with one small change.
Instead of passing an empty object (which obviously works, as {} !== {}), I would pass a boolean flag to my child, requesting to pass back the current timer value. As soon as the value is passed, I would then reset the flag to false, pass the value and wait for the next request by the parent.
Parent component:
const Parent = () => {
const [timerNeeded, setTimerNeeded] = useState(false);
const fooJustToTriggerChildAction = () => {
setTimerNeeded(true);
};
const handleValueFromChild = (timeFromChild) => {
console.log('Current time from child:', timeFromChild);
setTimerNeeded(false);
};
return (
<>
<Child
handleValueFromChild={handleValueFromChild}
timerNeeded={timerNeeded}
/>
<Button onPress={fooJustToTriggerChildAction} >
Click to take time
</Button>
</>
);
};
Child component
const Child = ({
timerNeeded,
handleValueFromChild,
}) => {
const [totalTime, setTotalTime] = useState(0);
const totalTimeRef = useRef(totalTime); // useRef to handle totalTime inside useEffect
const counter = () => {
totalTimeRef.current = totalTimeRef.current + 1;
setTotalTime(totalTimeRef.current);
setTimeout(counter, 1000);
};
useEffect(() => {
counter();
}, []); // Run time counter at first render
useEffect(() => {
if (timerNeeded) {
handleValueFromChild(totalTimeRef.current);
}
}, [timerNeeded]);
return (
<>
<div>Total time: {totalTime}</div>
</>
);
};
I'm working on a project that has to do with playlist, so what I want to execute is whenever one of the songs on the playlist is clicked the image that is attached to the song should be viewed.
So, I have my code this way...
const Component = () => {
const value = useContext(DataContext);
const [data, setData] = useState(null);
const [currentData, setCurrentData] = useState(null);
useEffect(() => {
const url =
"https://52-90-82-235.maverickmaven.com/geotourdata/json.cfm?h=-107,37,s,en,3A771765";
const currentValue = value;
axios({
method: "get",
url,
responseType: "stream",
}).then((response) => {
let features = response.data.features.filter((elem) => {
return elem.type === "Feature";
});
setData(features);
const currentDatafile = data?.filter((data) => {
return data?.assets[0].audio === value;
});
setCurrentData(currentDatafile);
});
}, [setCurrentData]);
};
So, what this code does is that it returns the array that has the picture, but the problem is that it only filters once and repeatedly returns the same value even if I click on another song, and I need it to filter every time I clicked on the songs(i.e the function is executed).
I tried filtering and mapping at the same time, but it didn't work. or maybe I didn't write the syntax well enough.
Please I need help.
Move these lines to new useEffect hook. Will trigger after you set data
useEffect(() => {
const currentDatafile = data?.filter((item) => {
return item.assets[0].audio === value;
});
setCurrentData(currentDatafile)},[data])
You shouldn't re-fetch the data from the remote source every time. I've wrapped that in a custom hook instead, here (and a custom fetcher function to make testing/mocking easier).
Then, you shouldn't hold the selected object in the state unless you need to modify it internally (in which case you should copy it into a state atom anyway); instead, just hold the ID.
function fetchTourData() {
return fetch('https://52-90-82-235.maverickmaven.com/geotourdata/json.cfm?h=-107,37,s,en,3A771765')
.then(response => response.json())
.then(data => data.features.filter((elem) => elem.type === 'Feature'));
}
function useTourData() {
const [data, setData] = React.useState(null);
React.useEffect(() => {
fetchTourData().then(setData);
}, [setData]);
return data;
}
const Component = () => {
const tourData = useTourData();
const [selectedId, setSelectedId] = React.useState(null);
const selectedTour = (tourData || []).find(t => t.id === selectedId);
if (tourData === null) {
return <div>Loading</div>
}
return (
<div>
<div>
Selected: {JSON.stringify(selectedTour || "nothing")}
</div>
<ul>
{tourData.map(t => <li key={t.id}><a href="#" onClick={() => setSelectedId(t.id)}>{t.name}</a></li>)}
</ul>
</div>
);
};
ReactDOM.render(<Component />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Hi I have a class component as shown below:
class SomeComponent extends Component {
componentDidMount = () => {
const divElement = document.getElementbyId('id'); // this element could take a few seconds to load
if (props.something1 && props.something2) {
..do something with divElement's width
}
}
render() {
return ....
}
}
I want to wait until divElement is loaded, or trigger an event when divElement is loaded so I can do my calculation later, tried adding setTimeout which did not work
Two answers for you:
Use a ref (if your component renders the element)
If the element is rendered by your component, use a ref.
Use a MutationObserver (if the element is outside React)
If the element is completely outside the React part of your page, I'd look for it with getElementById as you are, and if you don't find it, use a MutationObserver to wait for it to be added. Don't forget to remove the mutation observer in componentWillUnmount.
That would look something like this:
componentDidMount = () => {
const divElement = document.getElementbyId('id');
if (divElement) {
this.doStuffWith(divElement);
} else {
this.observer = new MutationObserver(() => {
const divElement = document.getElementbyId('id');
if (divElement) {
this.removeObserver();
this.doStuffWith(divElement);
}
});
this.observer.observe(document, {subtree: true, childList: true});
}
}
componentWillUnmount = () => {
this.removeObserver();
}
removeObserver = () => {
if (this.observer) {
this.observer.disconnect();
this.observer = null;
}
}
(You may have to tweak that, it's off-the-cuff; see the MutationObserver documentation for details.)
This is a dumb solution but it gets its jobs done:
const getElementByIdAsync = id => new Promise(resolve => {
const getElement = () => {
const element = document.getElementById(id);
if(element) {
resolve(element);
} else {
requestAnimationFrame(getElement);
}
};
getElement();
});
To use it:
componentDidMount = async () => {
const divElement = await getElementByIdAsync('id');
if (props.something1 && props.something2) {
// ..do something with divElement's width
}
}
You need to use the componentDidUpdate hook instead of componentDidMount hook. And better to use ref rather than getting div element by it's id:
componentDidUpdate() {
if (props.something1 && props.something2) {
// use divElementRef to interact with
}
}
The answer of #hao-wu is great. Just if anyone wonders how to use it with hooks here is my snippet.
const Editor = () => {
const [editor, setEditor] = useState<SimpleMDE | null>(null);
useEffect(() => {
(async () => {
const initialOptions = {
element: await getElementByIdAsync(id),
initialValue: currentValueRef.current
};
setEditor(
new SimpleMDE({
element: await getElementByIdAsync(id),
initialValue: currentValueRef.current
})
);
})();
}, [id]);
// Other effects that are looking for `editor` instance
return <textarea id={id} />;
};
Otherwise, the constructor of SimpleMDE cannot find an element and everything is broken :)
I guess you can adjust it to your use-case quite easily.
Most of the time useRef just works, but not in this scenario.
Here's my hook version to #hao-wu's answer
const useGetElementAsync = (query) => {
const [element, setElement] = useState(null);
useEffect(() => {
(async () => {
let element = await new Promise((resolve) => {
function getElement() {
const element = document.querySelector(query);
if (element) {
resolve(element);
} else {
console.count();
// Set timeout isn't a must but it
// decreases number of recursions
setTimeout(() => {
requestAnimationFrame(getElement);
}, 100);
}
};
getElement();
});
setElement(element);
})();
}, [query]);
return element;
};
To use it:
export default function App() {
const [isHidden, setIsHidden] = useState(true);
const element = useGetElementAsync(".myElement");
// This is to simulate element loading at a later time
useEffect(() => {
setTimeout(() => {
setIsHidden(false);
}, 1000);
}, []);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
{!isHidden && (
<h2 className="myElement">My tag name is {element?.tagName}</h2>
)}
</div>
);
}
Here's the codesandbox example
You can do something like;
componentDidMount() {
// Triggering load of some element
document.querySelector("#id").onload = function() {
// Write your code logic here
// code here ..
}
}
Reference https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/onload
When state is in a hook it can become stale and leak memory:
function App() {
const [greeting, setGreeting] = useState("hello");
const cb = useCallback(() => {
alert("greeting is " + greeting);
}, []);
return (
<div className="App">
<button onClick={() => cb()}>Click me</button>
<p>
Click the button above, and now update the greeting by clicking the one
below:
</p>
<button onClick={() => setGreeting("bye")}>
Update greeting
</button>
<p>Greeting is: {greeting}</p>
<p>
Now click the first button again and see that the callback still has the
old state.
</p>
</div>
);
}
Demo: https://codesandbox.io/s/react-hook-stale-datamem-leak-demo-9pchk
The problem with that is that we will run into infinite loops in a typical scenario to fetch some data if we follow Facebook's advice to list all dependencies always, as well as ensure we don't have stale data or memory leaks (as the example showed above):
const [state, setState] = useState({
number: 0
});
const fetchRandomNumber = useCallback(async () => {
if (state.number !== 5) {
const res = await fetch('randomNumber');
setState(v => ({ ...v, number: res.number }));
}
}, [setState, state.number]);
useEffect(() => {
fetchRandomNumber();
}, [fetchRandomNumber]);
Since Facebook say we should list fetchRandomNumber as a dependency (react-hooks/exhaustive-deps ESLint rule) we have to use useCallback to maintain a reference, but it regenerates on every call since it both depends on state.number and also updates it.
This is a contrived example but I've run into this many times when fetching data. Is there a workaround for this or is Facebook wrong in this situation?
Use the functional form of the state setter:
const fetchData = useCallback(async () => {
const res = await fetch(`url?page=${page}`);
setData((data) => ([...data, ...res.data]));
setPage((page) => page + 1);
}, [setData, setPage]);
Now you don't need data and page as your deps
You can also use a ref to run the effect only on mount :
const mounted = useRef(false);
useEffect(() => {
if(!mounted.current) {
fetchSomething();
mounted.current = true;
}
return () => { mounted.current = false }
}, [fetchSomething]);
And
const fetchSomething = useCallback(async () => {
...
}, [setData, setPage, data, page]);
fetchSomething is not a dependency here. You don't want to retrigger the effect, you only cause it once when the component mounts. Thats what useEffect(() => ..., []) is for.
I am having a React(version used 16.8) component, I have a const updateDiffText = useCallback(() callback on click of anchor GENERATE DIFF onclick onClick={updateDiffText} I call call back this updateDiffText
My requirement is I don't want one anchor code in my UI, I want whenever I have oldText and newText It should trigger the method updateDiffText and show the result. User should not click on anchor link to perform this.
My Code sand box here - https://codesandbox.io/s/react-diff-view-demo-htp06
if I have values in oldtext and newText it should call updateDiffText this method
My Code -
const DiffViewer = props => {
const oldText = useInput(props.startupConfigData);
const newText = useInput(props.runningConfigData);
const [{ type, hunks }, setDiff] = useState("");
const updateDiffText = useCallback(() => {
const diffText = formatLines(diffLines(oldText.value, newText.value), {
context: 3
});
const [diff] = parseDiff(diffText, { nearbySequences: "zip" });
setDiff(diff);
}, [oldText.value, newText.value, setDiff]);
const tokens = useMemo(() => tokenize(hunks), [hunks]);
return (
<div style={{ height: "450px", overflow: "auto" }}>
<a href="#" onClick={updateDiffText}>
GENERATE DIFF
</a>
{setDiff ? (
<Diff
viewType="split"
diffType={type}
hunks={hunks || EMPTY_HUNKS}
tokens={tokens}
>
{hunks => hunks.map(hunk => <Hunk key={hunk.content} hunk={hunk} />)}
</Diff>
) : (
""
)}
</div>
);
};
Let me know if query is not clear. Thanks.
Try to use the useEffect instead of useCallback. In your case you are are not calling the memoized function in the render stage. useCallback will return a memoized function. Check the modified version.
https://codesandbox.io/s/react-diff-view-demo-izdyi
const updateDiffText = useCallback(() => {
const diffText = formatLines(diffLines(oldText.value, newText.value), {
context: 3
});
const [diff] = parseDiff(diffText, { nearbySequences: "zip" });
setDiff(diff);
}, [props.startupConfigData, props.runningConfigData]);
to
const updateDiffText = useCallback(() => {
const diffText = formatLines(diffLines(oldText.value, newText.value), {
context: 3
});
const [diff] = parseDiff(diffText, { nearbySequences: "zip" });
setDiff(diff);
}, [oldText.value, newText.value, setDiff]);
////////////// Older solution before i understood ///////////////////////
////////////// New solution i suggest ///////////////////////////////////
const updateDiffText = () => {
// do what you wanna do
}
and use useEffect instead of useCallback like this
useEffect(() => {
updateDiffText();
},[props.startupConfigData, props.runningConfigData])