I have a react app which generates images on the front end dynamically using Plotly.js. I'd like to add image sharing functionality. I am trying to use react-share for this. Social platforms require image URL for image sharing and do not support images in base64 encoding or alike. Backend was implemented so it can receive images in base64, store in the database and return URL to the image, which is then used for sharing with react-share.
As the image is generated dynamically (it changes each time user resizes the chart, for instance), everything should be done when user clicks on Share icon.
So after the user has clicked on the Share icon, the image generated on the front end should be saved to back end
let imgURI;
const handleClick = () => {
Plotly.toImage('chartContainer', {
format: 'png',
width: 1000,
height: 600
})
.then(dataUrl => api.post('/image/base64ToPng', { image: dataUrl })
.then(
(response) => {
imgURI = response.data.imgURI;
},
failure => console.error(failure)
));
};
after the response is received, passed down to the sharing component like this
<FacebookShareButton
url={imgURI}
>
<FacebookIcon/>
</FacebookShareButton>
The code sample is not asynchronous, so the image URI is not passed to the sharing component, therefore sharing does not work. I tried to pass the prop down using conditional depending on whether it's defined or not and did not come up with a solution. I also looked up some issues in react-share repo that dealt with async urls, but seems like none of them deals with the dynamic image sharing on click.
I'd very appreciate a hint on how to complete this task.
This is serious hack territory, and the whole thing would be a lot simpler if this PR had been completed.
However, the code below should work (see codesandbox).
The key steps are:
Have a bit of state that keeps track of whether you have a url from the service or not.
When this state is "none", disable the facebook button's default behavior (i.e. openShareDialogOnClick = false)
Add an onClick handler to the facebook button that asynchronously fetches the url and sets the state (triggering a re-render)
Use an effect + ref so that when the url is set to something real, you manually call the click event on the button (which now has a real address in its url prop), and then re-sets the url to "none"
import { useEffect, useRef, useState } from "react";
import { FacebookIcon, FacebookShareButton } from "react-share";
async function getUrFromService(): Promise<string> {
// The real implementation would make a network call here.
await new Promise((resolve) => setTimeout(resolve, 1000));
return "https://via.placeholder.com/150";
}
export default function App() {
const shareButton = useRef<HTMLButtonElement>(null);
const [url, setUrl] = useState<string>("none"); // Unfortunately, we have to have a dummy string here, or FacebookShareButton will blow up.
// Provide an onClick handler that asyncronously fetches the url and sets it in the state.
const onClick = async () => {
// Be sure to check for the "none" state, so we don't trigger an infinite loop.
if (url === "none") {
const newUrl = await getUrFromService();
setUrl(newUrl);
}
};
// Whenever "url" changes and we re-render, we manually fire the click event on the button, and then re-set the url.
useEffect(() => {
if (url !== "none") {
shareButton.current?.click();
setUrl("none");
}
}, [url, shareButton]);
return (
<FacebookShareButton
ref={shareButton}
// Disable calling the dialog if we don't have a url yet.
openShareDialogOnClick={url !== "none"}
url={url}
onClick={onClick}
>
<FacebookIcon />
</FacebookShareButton>
);
}
Use navigator.share api if possible.
Having, said that you could write a wrapper that manages loading state and disables icon of component.
Create a async share component
//AsyncShareLoader.jsx
const AsyncShareLoader = ({ url, children }) => {
const loading = !url;
return (
<div style={{ filter: `grayscale(${loading ? "100%" : "0%"}` }}>
{React.Children.map(children, (child) =>
React.cloneElement(child, {
disabled: loading,
url: loading ? "none" : url,
openShareDialogOnClick: !loading
})
)}
</div>
);
};
Now, use this as a wrapper for your actual react-share icons. Something like this
const Share = () => {
const [url, setUrl] = useState()
useEffect(() => {
fetch('/imgUrl').then(getUrlFromRes).then(setUrl)
}, [])
return (
<AsyncShareLoader url={url}>
<FacebookShareButton>
<FacebookIcon />
</FacebookShareButton>
</AsyncShareLoader>
);
}
Extending this techinique you could manually steal click as Andrew suggested
const getBase64ImageFromUrl = url =>
fetch(url)
.then(response => response.blob())
.then(
blob =>
new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.onerror = reject;
blob = new Blob([blob], {type: 'image/png'});
reader.readAsDataURL(blob);
}),
);
const shareData = async () => {
await getBase64ImageFromUrl('https://url of image').then(base64 => {
const shareOption = {
title: App Link,
url: base64,
message:
'Download app now: ,AppLink:""',
};
try {
const shareResponse = Share.open(shareOption);
console.log('share response ,', shareResponse);
} catch (error) {
alert(error.message);
}
});
};
Related
Edit - added minimally reproducible example: https://snack.expo.dev/#hdorra/code
I hope everyone can access the snack. So if you add a task, you can see it show up in the log. Click on the circle, it shows as true (meaning it is clicked). Save and refresh and everything is stored (the task) but the checkbox is not. I stripped the code to make it as bare minimum as possible but it shows the problem.
It has been days of me on this error. I am relatively new to stackoverflow so my apologies if my question isn't clear or I am not asking it in the correct format. I am trying to create a to do app in react native that is using async storage. I created a toggle button that saves the toggle to a state. This button is located in a component:
const [checkBoxState, setCheckBoxState] = React.useState(false);
const toggleComplete = () => {
setCheckBoxState(!checkBoxState)
handleEdit();
console.log(checkBoxState)
}
When the user checks on it - seems to be showing up correctly as marked true and false in the console.
Then, this is passed to an edit handler to update the array, again console shows it is the correct state:
const handleEdit = () => {
props.editHandler(props.todoKey, text, checkBoxState);
console.log(text2, checkBoxState)
};
Then it shows that it saved correctly:
const [todos, setTodos] = React.useState([]);
const handleEdit = (todoKey, text, newStatus) => {
const newTodos = [...todos];
const index = newTodos.findIndex(todos => todos.key === todoKey);
newTodos[index] = Object.assign(newTodos[index], {title: text, status: newStatus});
setTodos(newTodos);
console.log(todos, newStatus)
};
The async function to save to the device and load are as follows:
To save:
const saveTodoToUserDevice = async (todos) => {
try {
const stringifyTodos = JSON.stringify(todos);
await AsyncStorage.setItem('todos', stringifyTodos);
} catch (error) {
console.log(error);
}
};
To load from the device:
const getTodosFromUserDevice = async () => {
try {
const todos = await AsyncStorage.getItem('todos');
if (todos != null) {
setTodos(JSON.parse(todos));
console.log("loaded successfully");
}
} catch (error) {
console.log(error);
}
};
So here is the issue - I get the console log that says it is saved correctly and loaded. BUT, when I refresh, the checkbox state is not saved at all, just the title text (so it is saving but the checkbox would always be false (the initial state set). If I clicked on true, it would show as true and then when I refresh, it goes back to false.
I have spent days and days on this and can't figure it out. Any direction would be helpful Thank you!
I have gone through your code and found some errors you are making in different places. In Task.js you can do without that checkBoxState. For that, pass the status to Task as props while rendering it in FlatList, like so:
<Task
key={item.key}
todoKey={item.key}
title={item.title}
status={item.status}
editHandler={handleEdit}
pressHandler={handleDelete}
/>
Then as below, change the button to toggle the status, so you use what's coming from the props and create a function called toggleStatus and pass it to onPress:
<TouchableOpacity onPress={toggleStatus}>
<View
style={[
styles.circle,
!props.status ? styles.completeCircle : styles.incompleteCircle,
]}
></View>
</TouchableOpacity>
The code for toggleStatus:
const toggleStatus = () => {
props.editHandler(props.todoKey, props.title, !props.status);
};
And handleEdit would be simplified to:
const handleEdit = () => {
props.editHandler(props.todoKey, text2, props.status);
setEdit(false);
console.log(props.status);
};
Lastly, in TasksMain.js so you don't replace what's in the storage with that initial array given to useState, make sure saveTodoToUserDevice runs after getTodosFromUserDevice. For that, add the below state in TasksMain.js and slightly change the two functions as follow:
const [loading, setLoading] = React.useState(true);
const saveTodoToUserDevice = async (todos) => {
if (loading) return;
try {
const stringifyTodos = JSON.stringify(todos);
await AsyncStorage.setItem("todos", stringifyTodos);
} catch (error) {
console.log(error);
}
};
const getTodosFromUserDevice = async () => {
try {
const todos = await AsyncStorage.getItem("todos");
if (todos != null) {
setTodos(JSON.parse(todos));
console.log("loaded successfully");
}
} catch (error) {
console.log(error);
} finally {
setLoading(false);
}
};
I have a problem with uploading an image. I have cards and all cards have an image. When I clicked Update Product, Card Details page is opening.
After I click this image, I choose another picture, the photo changes in Front, and when I clicked submit API gives me success and the correct photo URL.
But when I submitted this form, I returned all cards page ( with history.push()), the image was not changed. If I clicked empty cache and hard reload in my browser after the new image was changed. I tried to fix and first of all, I looked network and the response gave me the correct link, I opened a new tab and put the link and a new image opened. Maybe the link between old and new images is the same and useState is not working.
Api gives a new link, useState gives the old link. Both links are the same.
I tried window.location.reload(true), but it did not work first uploading. After first cleaning the cache, the image was uploaded, and after the second uploading, it worked without cleaning the cache.
My codes
It is for getting one card's data and image
const [getPhoto, setGetPhoto] = useState();
useQuery(["PRODUCT_DATA_OVERVIEW_BRANCH", id], () =>
apiService.getAdminProductDetails(id), {
onSuccess: (response) => {
setGetPhoto(response.photoUrl);
},
});
Changing data Api
const { mutate, isLoading } = useMutation(apiService.updateAdminProducts, {
onSuccess: () => {
enqueueSnackbar("Product was updated", { variant: "success" });
history.push("/admin/products");
window.location.reload(true);
},
});
onSubmit
const onSubmit = () => {
const urlProductInfo = new URLSearchParams(getProductData);
let formData = new FormData();
formData.append("photo", getPhoto);
mutate({ urlProductInfo, formData });
};
api
const ApiService = (httpService) => {
updateAdminProducts: async (variables) => {
const { urlProductInfo, formData } = variables;
const response = await httpService.patch(`/product/sme?${urlProductInfo}`, formData);
return response?.data;
},
}
I am using the react-dropzone library to drag or select files from the browser.
I have the below component to let the user select multiple files. Everything works except that if the user chooses more files let say 2 or more files with size 1 MB, the selection of documents is taking time. If the number of files is more, the more time it takes to selected the user-selected files.
As per my reading of the react-dropzone docs, it processes files as soon as it uploads. So I tried setting autoProcessQueue='false'.
but no luck.
I just want to let the user select all the files without blocking for 30 or 40 secs after selecting files, and before send to the backend server.
I did the debug and the setSelectedUserFiles() is reached after all the files are processed internally by the DropZone. I am not sure if there is a way to disable it and let it process as part final submission of the form or click on the button.
In case if we can't achieve the same, is there a way to show the message to the user that files are being attached.
Any help will be appreciated.
Below is my react component
const SelectUserFiles = () => {
const [userName,setUserName] = userState('TestUser')
const [selectedUserFiles,setSelectedUserFiles] = userState([])
const handleUserFileUpload = async (acceptedFiles) => {
await setSelectedUserFiles(acceptedFiles)
}
return (
<div className='myClass'>Select Files</div>
<Dropzone
//autoProcessQueue='false'
accept={'.pdf'}
onDrop={acceptedFiles => handleUserFileUpload(acceptedFiles)}
>…</Dropzone>
</div>
<MyButton>
//logic to send the files to backend axios with the files selectedUserFiles
</MyButton>
)
}
Try something like this
Create separate api component that only send files,
Your component that has dropzone manages file upload
locally without sending to the server. You only click the button
and send it.
this link was very helpful:
https://www.robinwieruch.de/react-hooks-fetch-data
first component:
export const useApiCall = () => {
const [data, setData] = useState({});
const [selectedAllUserFiles, setSelectedAllUserFiles] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
useEffect(() => {
const pushToserver = async () => {
setIsError(false);
setIsLoading(true);
try {
const formData = new FormData();
Array.from(selectedAllUserFiles).forEach((file) => {
form.append('fileData', file);
});
const result = await axios.post(someUrl, formData);
setData(result.data);
} catch (error) {
setIsError(true);
}
setIsLoading(false);
};
if (selectedAllUserFiles.length > 0) {
pushToserver();
}
}, [selectedAllUserFiles]);
//passing reference to the calling component
return [{ data, isLoading, isError }, setSelectedAllUserFiles];
};
Second component:
const SelectUserFiles = () => {
const [userName,setUserName] = userState('TestUser')
const [selectedUserFiles,setSelectedUserFiles] = userState([])
//Ref from logical api call component
const [{ data, isLoading, isError }, setSelectedAllUserFiles] = useApiCall();//new component
const handleUserFileUpload = async (acceptedFiles) => {
if (acceptedFiles) {
acceptedFiles.map((file) => {
setSelectedUserFiles((selectedUserFiles) => selectedUserFiles.concat(file));
return selectedUserFiles;
});
}
//await setSelectedUserFiles(acceptedFiles)
}
return (
<div className='myClass'>Select Files</div>
<Dropzone
//autoProcessQueue='false'
accept={'.pdf'}
onDrop={acceptedFiles => handleUserFileUpload(acceptedFiles)}
>…</Dropzone>
</div>
//on click call api and pass collected user files all together
<MyButton onClick={()=>setSelectedAllUserFiles(selectedUserFiles)}>
//logic to send the files to backend axios with the files selectedUserFiles
</MyButton>
)
}
I have a file that stores an array of objects. I have a component that fetches data from this file then render the list. The file could be updated somewhere else, I need the component to be updated if the file is modified. I have following code example
const header = () => {
const [list, setList] = useState([]);
// fetch
useEffect(() => {
const loadList = async () => {
const tempList = await getList("/getlist"); // get call to fetch data from file
setList(tempList);
};
loadList ();
}, [list]);
// function to render content
const renderList = () => {
return list.map(obj => (
<div key={obj.name}>
{obj.name}
</div>
));
};
return (
<div>{renderList()}</div>
)
}
// get call
router.get('/getlist',
asyncWrapper(async (req, res) => {
const result = await getList();
res.status(200).json(result).end();
})
);
const getList= async () => {
const list = JSON.parse(await fs.readFile(listPath));
return list;
}
Code has been simplified. If I remove the list from useEffect, then it will only render once and will never update unless I refresh the page. If I include list there, loadList() will get called constantly, and component will get re-rendered again and again. This is not the behavior I want. I am just wondering without making header component async component, how do I only re-render this component when the file is changed?
Thank you very much.
There are two approaches you can take to this:
Polling
Request the URL on an interval, and clear it when the component is unmounted.
Replace loadList () with:
const interval = setInterval(loadList, 60000); // Adjust interval as desired
return () => clearInterval(interval)
Make sure the cache control headers set in the response to /getlist don't stop the browser from noticing updates.
Server push
Rip out your current code to get the data and replace it with something using websockets, possibly via Socket.IO. (There are plenty of tutorials for using Socket.io with React that can be found with Google, but its rather too involved to be part of a SO answer).
I need redirect page after it load and get param from URL. I can do by button click. But how I can make page redirect automatic after page have get param from URL (without user input)?
Thanks!
const handleClick = async (event) => {
const stripe = await stripePromise;
const { error } = await stripe.redirectToCheckout({
param,
});
}
const param = new URLSearchParams(window.location.search).get('param');
const Page = () => {
React.useEffect(() => {
window.scrollTo(0, 0);
}, []);
return (
<section >
<button role="link" onClick={handleClick}>
Press
</button>
</section>
);
};
export default Page;
You could probably just use your useEffect hook there to redirect the page, instead of using the onClick handler. e.g.
const handleLoad = async () => {
const stripe = await stripePromise;
const { error } = await stripe.redirectToCheckout({
param,
});
}
const param = new URLSearchParams(window.location.search).get('param');
const Page = () => {
React.useEffect(() => {
handleLoad();
}, []);
return null;
};
export default Page;
Though, you don't really even need to load up React for any of this. You could just make this page a static html page with a script tag that does the redirect logic, or something similar to that.
After further discussion, we found out that in order for this stripe stuff to work on Safari-mobile, we needed to wait for the page's load event before calling handleLoad(), so something like this:
const handleLoad = async () => {
const stripe = await stripePromise;
const { error } = await stripe.redirectToCheckout({
param,
});
}
const param = new URLSearchParams(window.location.search).get('param');
window.addEventListener('load', () => handleLoad())
You're going to want to parse those query parameters in the init/load handler. You can see an example of this here. Note that the event listener on DOMContentLoaded kicks everything off. An example of this in action can be seen here.
I noticed that your code uses param in the Checkout redirect directly. If param is a checkout session that you should be generating these on demand, not sending them in emailed links etc, as they are short-lived. In the example above, the code param is read and use to generate a new checkout session via a fetch and then that is used for the redirect.