I want to show a notification Modal if an image which I want to upload is too large. My problem is, that the handleUpload function is an external function. How can I render the modal?
My parent component:
const MyParent = () => {
const boundHandleUpload = React.useCallback(handleUpload);
return <UploadInput
onChange={boundHandleUpload}
accept="image/png,image/jpeg,image/gif,.jpg,.png,.jpeg,.gif"
/>
}
My upload input component
interface MyProps
extends React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement> {
}
export const UploadInput = ({
onChange,
}: MyProps) => {
const inputRef = React.useRef<HTMLInputElement>(null);
return <input ref={inputRef as any} onChange={onChange} />
};
and my handleUpload function:
export const handleUpload = (evt: React.FormEvent<HTMLInputElement>): void => {
const target = evt.target as HTMLInputElement;
let file, img;
if ((file = target.files[0])) {
img = new Image();
img.onload = function () {
if(this.width > 1000) alert("Show Modal");
};
img.src = window.URL.createObjectURL(file);
}
};
How could I show a React component Modal instead of the alert?
Your function, even if external, must know about React state if you want it to work with React.
Just wrap your function handleUpload inside another function, to save setShowModal in the closure.
This way, you can edit the state of your component!
export const handleUpload = setShowModal => (evt: React.FormEvent<HTMLInputElement>): void => {
const target = evt.target as HTMLInputElement;
let file, img;
if ((file = target.files[0])) {
img = new Image();
img.onload = function () {
if(this.width > 1000) setShowModal(true);
};
img.src = window.URL.createObjectURL(file);
}
};
Now, handleUpload is a function that returns your event listener, so you just call it by passing setShowModal and it will return the same thing than before, but with the access to the state!
const MyParent = () => {
const [showModal, setShowModal] = useState(false);
const boundHandleUpload = React.useCallback(handleUpload(setShowModal));
return <>
<UploadInput
onChange={boundHandleUpload}
accept="image/png,image/jpeg,image/gif,.jpg,.png,.jpeg,.gif"
/>
{showModal && <Modal/>}
</>
}
Hope this is clear, feel free to ask if it is not!
Related
In my React code I have to use a legacy component, which makes a setup api call when it is first rendered. The component has a custom completion/cancelation event which I use to trigger a State update. The current Code looks like this:
export const useOneTimePassword = (
headline = "OTP anfordern",
id = "opt",
type = "sms",
businessProcess = "otp-process"
): UseOneTimePasswordReturn => {
const [otpCode, setOtpCode] = useState<undefined | string>();
const [isOtpCancelled, setIsOtpCancelled] = useState<boolean>(false);
const openOtp = () => {
const otp = document.querySelector(`otp-component#${id}`) as OtpElement;
otp.open();
};
const OtpComponent: FC = () => (
<Otp
headline={headline}
id={id}
type={type}
businessProcess={businessProcess}
setIsOtpCancelled={setIsOtpCancelled}
setOtpCode={setOtpCode}
/>
);
return {
otpCode,
isOtpCancelled,
openOtp,
OtpComponent,
removeOtp: () => {
setOtpCode(undefined);
},
};
};
and for the Component it looks like this:
const Otp: React.FC<OtpProps> = ({
headline,
businessProcess,
type,
id,
setOtpCode,
setIsOtpCancelled,
}) => {
function onOtpResponse(e: CompletedEvent) {
if (e.detail.otpCode) {
setOtpCode(e.detail.otpCode);
setIsOtpCancelled(false);
} else {
setIsOtpCancelled(true);
}
}
const ref = useRef();
useEffect(() => {
//#ts-ignore
if (ref.current) ref.current.addEventListener("completed", onOtpResponse);
}, []);
return (
<otp-component
ref={ref}
headline={headline}
id={id}
type={type}
business-process={businessProcess}
/>
);
};
export default Otp;
What I do not understand is that state changes in otpCode aswell as isOtpCancelled cause a rerender of the OtpComponent
I want to call child function from parent and set state of a child's hook ,but I cant able to success it ,simple code is below ,setstate isnt working inside useImperativeHandle.Any help is appreciated ,thx..
const child = forwardRef((props,ref) => {
const [pagerTotalCount, setPagerTotalCount] = useState(0);
const [customerData, setCustomerData] = useState([]);
useImperativeHandle(ref, () => ({
childFunction1: updatePagerTotalCount;
}));
})
const updatePagerTotalCount = (param) => {
setPagerTotalCount(param); // this IS working now...
const pagerInputModel = {
"pageNumber": 1,
"pageSize": 3,
};
myservice.listCustomerList(pagerInputModel).then((res) => {
const { isSuccess, data: customers} = res;
if (isSuccess) {
console.log("api result:" + JSON.stringify(customers)); // this IS working,api IS working
setCustomerData(customers); // this IS NOT working , cant SET this.
console.log("hook result:" + JSON.stringify(customerData)); //EMPTY result.I tested this WITH another buttonclick even IN ORDER TO wait FOR async,but still NOT working
}
});
};
const parent= () => {
const childRef = React.useRef(null)
const handleClick = () => {
childRef.current.childFunction1(11); //sending integer param to child
};
RETURN(
<>
<Button variant="contained" endIcon={<FilterAltIcon />} onClick={handleClick}>
Filtrele
</Button>
<child ref={childRef}/>
</>
)
}
You should define a function to update the state and return that function via useImperativeHandle.
const updatePagerTotalCount = (param) => {
setPagerTotalCount(param);
};
useImperativeHandle(ref, () => ({
childFunction1: updatePagerTotalCount;
}));
Now with above when childRef.current.childFunction1(11); is invoked via parent component, you can see the state is being set correctly.
I was creating some header for my app, so I tried to place the hooks in some other file for better organization.
But I'm a bit confused, how do I call this function useHeaderNavStatus() when I click the button in the header tag, without creating another useEffect and useState in Header component? Is it possible or I'm too blind?
I appreciate any help! :D
Here is the file structure:
Header Component
const Header = () => {
const headerNav = useHeaderNavStatus();
return (
<header>
<button
ref={headerNav.toggleRef}
onClick={What do I do here?}>
Menu
</button>
</header>
);
}
Hooks file
import { useEffect, useRef, useState } from 'react';
const useHeaderNavStatus = () => {
// Creating ref for toggle button
const toggleRef = useRef<HTMLButtonElement>(null);
// Creating state to know if nav is showing or not
const [isActive, setIsActive] = useState(false);
// This function opens and closes nav
const updateBodyScroll = () => {
setIsActive(!isActive);
const body = document.body.classList;
if (isActive) {
body.remove('no-scroll');
} else {
body.add('no-scroll');
}
};
// This function closes the nav on outside click
const closeNavOnOutsideClick = (event: MouseEvent) => {
if (!toggleRef.current?.contains(event.target as Node))
updateBodyScroll();
};
// Adds and removes event listener to know the target click
useEffect(() => {
if (isActive) {
window.addEventListener('mousedown', closeNavOnOutsideClick);
return () =>
window.removeEventListener('mousedown', closeNavOnOutsideClick);
}
});
return { toggleRef, isActive };
};
Your hook should also return a function that opens the nav:
const openNav = () => setIsActive(true);
return { toggleRef, isActive, openNav };
And then use it:
const Header = () => {
const headerNav = useHeaderNavStatus();
return (
<header>
<button
ref={headerNav.toggleRef}
onClick={headerNav.openNav}>
Menu
</button>
</header>
);
}
I have a component that changes the background image depending on the state. I added simplified codes down below.
Since I fetch an image from the server on state changes, the background image was flashing. This is the reason I load them to DOM with preloadImage() function. This function solved the issue.
The problem starts with testing. See the testing file!
const BackgroundImage = styled`
...
background-image: ${(props) => props.bg && `url(${props.bg})`};
`
const preloadImage = (src, wrapperRef, callback) => {
const img = new Image();
img.src = src;
img.style.display = 'none';
img.dataset.testid = 'preloaded-image';
const el = wrapperRef.current;
el.innerHTML = '';
el.appendChild(img);
img.onload = () => typeof callback === 'function' && callback(src);
};
const Panel = (defaultBG) => {
const imageCacheRef = useRef();
const [bg, setBG] = useState(defaultBG);
useEffect(() => {
const fetchImage = async () => {
const imageSrc = await import(`https://fakeimageapi.com/${bg}.png`);
return preloadImage(imageSrc.default, imageCacheRef, setImage);
}
try {
await fetchImage()
} catch (error) {
console.log(error)
}
}, [])
return (
<div ref={imageCacheRef}>
<BackgroundImage bg={bg} data-testid="bg" />
<button onClick={ () => setBG('cat') }>Cat</button>
<button onClick={ () => setBG('dog') }>Cat</button>
<button onClick={ () => setBG('rabbit') }>Cat</button>
<button onClick={ () => setBG('parrot') }>Cat</button>
</div>
)
}
This is the test suite written with Testing Library.
import { render, waitFor, screen, act } from '#testing-library/react';
describe('Panel', () => {
test('Sets background-image correctly', async () => {
render(<Panel defaultBG="panda" />)
expect(screen.getByTestId('bg')).toHaveStyle(
'background-image: url(panda.png);',
);
})
})
Unfortunately, this test fails. The problem (I guess) that I use a callback after the image is loaded inside useEffect. How can I final this test with a successful result?
The problem is solved. I added a test-id to the image inside preloadImage() and loaded the image with the fireEvent method. That's it!
import { render, waitFor, screen, fireEvent } from '#testing-library/react';
describe('Panel', () => {
test('Sets background-image correctly', async () => {
render(<Panel defaultBG="panda" />)
const image = await waitFor(() => screen.getByTestId('preloaded-image'));
fireEvent.load(image);
expect(screen.getByTestId('bg')).toHaveStyle(
'background-image: url(panda.png);',
);
})
})
Also, some refactoring on preloadImage() function.
const preloadImage = (src, wrapperRef, callback) => {
const img = new Image();
img.src = src;
img.style.display = 'none';
img.dataset.testid = 'preloaded-image';
const el = wrapperRef.current;
el.innerHTML = '';
el.appendChild(img);
if (typeof callback === 'function') {
img.onload = () => callback(src);
}
};
I am receiving an undefined error when trying to set canvasRef.current. I have tried many different ways to form a callback ref, but I am getting no luck. How can I wait to fire the onClick function 'handleViewStuff' AFTER canvasRef.current is not undefined?
const Child = (props) => {
const canvasRef = useRef();
const handleViewStuff = useCallback(() => {
apiCall(id)
.then((response) => {
// do stuff
return stuff;
})
.then((result) => {
result.getPage().then((page) => {
const canvas = canvasRef.current;
const context = canvas.getContext('2d'); // error is coming in here as getContext of undefined meaning canvas is undefined'
canvas.height = 650;
const renderContext = {
canvasContext: context,
};
page.render(renderContext);
});
});
}, []);
return (
<Fragment>
<canvas ref={(e) => {canvasRef.current = e}} />
<Button
onClick={handleViewStuff}
>
View Stuff
</Button>
</Fragment>
);
};
export default Child;
Using if-statement
...
if(canvas.current) {
const canvas = canvasRef.current;
const context = canvas.getContext('2d');
}
Using optional chaining
...
const canvas = canvasRef?.current;
const context = canvas?.getContext('2d');
And I found some mistakes in your code.
add dependencies on useCallback
const handleViewStuff = useCallback(() => {
...
}, [canvasRef.current]);
should use ref like this.
<canvas ref={canvasRef} />