Upload Images to Firestore then Set Formik Field Value - reactjs

I am uploading files to Firestore using this custom upload hook I created:
import { storage } from "../firebase/config"
import { ref, getDownloadURL, uploadBytes } from "firebase/storage"
export const useUpload = () => {
const upload = async (folder, file) => {
const fileToUpload = ref(storage, `images/${folder}/${file.lastModified + file.name}`)
const url = await uploadBytes(fileToUpload, file).then(async (snapshot) => {
const imgURL = await getDownloadURL(snapshot.ref)
return imgURL
})
return url;
}
return { upload }
}
I imported the useUpload hook in another file and I'm using it like this:
const {upload} = useUpload()
const previewFiles = (items) => {
items.map(async (file) => {
setIsLoading(true)
await upload('propertyImages', file).then(url => {
return Object.assign(file, { preview: url })
})
setIsLoading(false)
})
return items.map(({preview}) => preview)
}
I use dropzone to process the files so that the users can preview the uploaded files thus:
const { getRootProps, getInputProps } = useDropzone({
accept: {
'image/*': []
},
onDrop: acceptedFiles => {
setFiles(previewFiles(acceptedFiles))
}
});
Then with this useEffect hook, I set the formik field value:
useEffect(() => {
formik.setFieldValue("images", files.map(file => (file)))
return () => files.forEach(file => URL.revokeObjectURL(file));
}, [files]);
Here's the Formik function:
const formik = useFormik({
enableReinitialize: true,
initialValues: {
images: []
},
validationSchema: Yup.object({
images: Yup.array().min(1, "Please upload at least one image").required()
}),
onSubmit: values => {
console.log(values);
}
})
The problem now is that when I submit the Formik form, the images are returning undefined. I tried to return the whole file, instead of returning only the image urls, and they work properly. Here's what I tried:
//To Upload the files
const previewFiles = (items) => {
items.map(async (file) => {
setIsLoading(true)
await upload('propertyImages', file).then(url => {
return Object.assign(file, { preview: url })
})
setIsLoading(false)
})
return items
}
//To Set the formik values
useEffect(() => {
formik.setFieldValue("images", files.map(file => (file)))
return () => files.forEach(file => URL.revokeObjectURL(file.preview));
}, [files]);
This returns the File object with the preview but if I use this useEffect, I get undefined:
useEffect(() => {
formik.setFieldValue("images", files.map(file => (file.preview)))
return () => files.forEach(file => URL.revokeObjectURL(file));
}, [files]);
Please what am I doing wrong? Any help at all would be greatly appreciated as I have been on this issue for many days!

I found out that I could just create a state to hold the values of the urls and set the values as soon as the images are done uploading.
It's something I've always known, I just didn't figure it out in time. So here's the solution I used:
const [urls, setUrls] = useState([])
const previewFiles = (items) => {
items.map(async (file) => {
setIsLoading(true)
await upload('propertyImages', file).then(url => {
setUrls(prev => [...prev, url])
return Object.assign(file, { preview: url })
})
formik.setFieldValue("images", urls)
setIsLoading(false)
})
return items
}
I only modified the previewFiles function.

Related

Change data on two different querys from React Query

so im doing a little application of a pokedex utilizing the pokeapi to test react query, what i am trying to do is first fetch the data on mount and after that, if i use the search button the data already fetched to change something like this
fetch on mount
and the search
search fetch
something easy to do with useState but i am having problems with react query
i got something like this
pokedex
at the mount of the component i have this
export const fetchPokemon = async (URL: string) => {
const result = await axios.get<pokemon>(URL);
return result;
};
export const fetchPokemons = async () => {
const URL = "https://pokeapi.co/api/v2/pokemon?limit=20&offset=0s";
const { data } = await axios.get<pokemons>(URL);
const result = await Promise.all(
data.results.map(async (pokemon) => {
return fetchPokemon(pokemon.url);
})
);
return result;
};
export const useAllPokemons = () => {
return useQuery({
queryKey: ["pokemons"],
queryFn: fetchPokemons,
});
};
const { data, isLoading } = useAllPokemons();
works great but now i want to search pokemons with a search button like in the image and to replace the initial data that i already fetch so only the data that i searched appears so i did this
export const fetchAllPokemons = async () => {
const URL = "https://pokeapi.co/api/v2/pokemon?limit=100";
const { data } = await axios.get<pokemons>(URL);
const result = await Promise.all(
data.results.map(async (pokemon) => {
return fetchPokemon(pokemon.url);
})
);
return result;
};
let { data, refetch } = useQuery({
queryKey: ["pokemons"],
queryFn: fetchAllPokemons,
enabled: false,
select: (data) => {
const pokemonData = data.map((pokemon) => {
if (pokemon.data.name.startsWith("char")) {
return pokemon;
}
});
return pokemonData;
},
});
<button
onClick={() => {
refetch();
}}
>
asd
</button>
and nothing happens, but when i open the console the data is changing but then again returns to the initial fetch
I guess you should use the onSuccess:
const {data} = useQuery("fetchData", fetchData, {
onSuccess: (data) => {
// do something with your data and return
}
});

data is not reloading after being saved

I'm storing data using API, it saves successfully, but it's not reloading in the datagrid. I have manually refresh the entire page to view the data. I tried to put the storing variable in a function, then call that function, but it's not rendering I guess. kindly help me. Thank you in advance
here's the code
const ContactDataGrid = ({ rows, columns }) => {
const [platform, setPlatform] = useState([]);
const [searchText, setSearchText] = useState('');
const [Rows, setRows] = useState([]);
const [open, setOpen] = useState(false);
const [formInputData, setformInputData] = useState(
{
name: '',
details: '',
}
);
const handleOpen = () => setOpen(true);
const handleClose = () => setOpen(false);
const handleChange = (evnt) => {
setOpen(true)
const newInput = (data) => ({
...data,
[evnt.target.name]: evnt.target.value
});
setformInputData(newInput);
}
const showData = () => setRows(rows);
useEffect(() => {
setPlatform(rows);
showData();
}, [rows]);
console.log()
const handleSubmit = (evnt) => {
evnt.preventDefault();
const formData = new FormData(); //formdata object
formData.append('nickname', formInputData.nickname); //append the values with key, value pair
formData.append('target', formInputData.target);
const config = {
headers: { 'content-type': 'multipart/form-data' }
}
axios.post('http://localhost:8006/api/v2/save/beneficiary', formData, config)
.then(response => {
if (response.data.success === true) {
showData()
alert(response.data.message)
}
})
.catch(error => {
alert(error.message);
});
setformInputData({ nickname: "", target: "" });
setOpen(false);
}
function escapeRegExp(value) {
return value.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
}
const requestSearch = (searchValue) => {
const searchRegex = new RegExp(escapeRegExp(searchValue), 'i');
const filteredRows = platform.filter((row) => {
return Object.keys(row).some((field) => {
return searchRegex.test(row[field]?.toString() || '');
});
});
setRows(filteredRows);
};
I think when you request success the variable rows don't change so show data load the old data. You can trigger when post data you request fetch data again and change datagrid.

How do I upload an array of image URLs to Firestore?

I am using Formik to handle a form submission to create new real estate properties, which includes image upload. I have everything else working: I have a function that first uploads all of the images to storage and returns the image links (the getLinks function), and then we add those URLs to the formik values object to try to upload them all to my forestore. This is the whole function.
const getLinks = async (values) => {
const array = [];
for await (const file of rawFiles) {
const storageRef = ref(storage, `/houses/${file.name}`);
uploadBytes(storageRef, file).then((snapshot) => {
getDownloadURL(snapshot.ref).then((url) => array.push(url));
});
}
return array;
};
onSubmit: async (values) => {
getLinks(values)
.then((imageArray) => {
const newVals = { ...values, imageList: imageArray };
return newVals;
})
.then(async (newValues) => {
console.log(newValues);
const docRef = await addDoc(collection(db, "properties"), newValues);
})
.finally(() => {
setSnackAlert({
type: "success",
message: "You provided values!! Congrats!",
});
handleOpen();
})
.catch((err) => {
setSnackAlert({
type: "error",
message: "There was an error handling your request",
});
handleOpen();
});
The frustrating part of this, however, is that the console log directly before we submit the values to my firestore CORRECTLY logs the object with the new image URLs. Here is the console log (I purposely cut it off for sensativity, but this array does have two images)
But it does not send them up to firestore. Instead, this is what I get:
Any help is much appreciated!
Here is some replication code to attempt such a problem yourself. The form is not as lengthy, but should work the same. You will need your own firebase info to try and replicate it.
import React, {useState} from 'react';
import './App.css';
import {useFormik} from 'formik'
import { collection, addDoc } from "firebase/firestore";
import { ref, uploadBytes, getDownloadURL } from "firebase/storage";
// YOU WILL NEED TO IMPORT YOUR OWN FIREBASE INFORMATION HERE FOR REFERENCE.
function App() {
const [images, setImages] = useState([])
const selectMultipleFiles = (e) => {
const raw = [];
const newImages = [];
raw.push(e.target.files);
for (let i = 0; i < raw[0].length; i++) {
newImages.push(URL.createObjectURL(raw[0][i]));
}
setImages(newImages);
};
const imageDisplay = images.map((image) => {
return <img src={image} style={{height: "50px", aspectRatio: "16 / 9"}}/>
})
const formik = useFormik({
initialValues: {
address: "",
price: null,
},
onSubmit: async (values) => {
getLinks(values)
.then((imageArray) => {
const newVals = { ...values, imageList: imageArray };
return newVals;
})
.then(async (newValues) => {
console.log(newValues);
const docRef = await addDoc(collection(db, "properties"), newValues);
})
.finally(() => {
alert("You provided values!! Congrats!")
});
})
.catch((err) => {
alert("Sorry, there was an error.")
});
},
})
return (
<main>
<h1>StackOvervlow Replication</h1>
<form onSubmit={formik.handleSubmit}>
<input
type="text"
name="address"
value={formik.values.address}
onChange={formik.handleChange}
/>
<input
type="number"
name="price"
value={formik.values.price}
onChange={formik.handleChange}
/>
<label htmlFor="raised-button-file">
<input
accept="image/*"
type="file"
multiple
onChange={selectMultipleFiles}
/>
</label>
<button type="submit">Submit</button>
{imageDisplay}
</form>
</main>
);
}
export default App;
The trick here is that getDownloadURL() and uploadBytes() is an async function that happens to return a promise (as stated in the documentation). You can use await() to make the code execute more synchronously. See code below:
const getLinks = async (values) => {
const array = [];
for await (const file of rawFiles) {
const storageRef = ref(storage, `/houses/${file.name}`);
const upload = await uploadBytes(storageRef, file);
const imageUrl = await getDownloadURL(storageRef);
array.push(imageUrl);
}
return array;
};
The above code will return an array instead of a promise.
I think you want to use arrayUnion()
const newVals = {...values, imageList: arrayUnion(...imageArray)}
FieldValue arrayUnion and Cloud FireStore with Flutter

Refactoring to Fetch API only once React.JS

I am building a Project with the Pokemon API. Here it is how I am fetching the data:
pokeAPI.js
export const api = {
getPokemonList: async ({ url }) => {
return new Promise((resolve) => {
fetch(url)
.then(res => res.json())
.then(data => {
resolve(data)
})
});
},
getPokemonInfo: async (url) => {
return new Promise((resolve) => {
fetch(url)
.then(res => res.json())
.then(data => {
resolve(data)
})
});
}
};
App.js
const [pokemon, setPokemon] = useState([]);
const URL = 'https://pokeapi.co/api/v2/pokemon?limit=150';
useEffect(() => {
const getPokemonInfo = async () => {
const json = await api.getPokemonInfo(URL);
await loadPokemon(json.results);
};
getPokemonInfo();
}, []);
const loadPokemon = async (data) => {
let pokemonData = await Promise.all(data.map(async pokemon => {
let pokemonList = await api.getPokemonList(pokemon)
return pokemonList
}))
setPokemon(pokemonData);
};
Although this works, it's currently calling getPokemonList for every pokemon and the fact that there are multiple async / await is not helping with readiability. How could I refactor this logic:
const loadPokemon = async (data) => {
let pokemonData = await Promise.all(data.map(async pokemon => {
let pokemonList = await api.getPokemonList(pokemon)
return pokemonList
}))
setPokemon(pokemonData);
};
to fetch only once, using a memoized value in a hook to prevent multiple re-renders?
Thanks for helping.
`there are several ways to do it, you can use redux like state managements or browsers local storage
const App = () => {
const [pokemons, setPokemons] = useState([]);
useEffect(() => {
let pokemons= localStorage.getItem("users");
if (pokemons) {
pokemons = JSON.parse(pokemons);
setPokemons({ pokemons });
} else {
fetch("https://pokeapi.co/api/v2/pokemon?limit=150")
.then(res => res.json())
.then(pokemons => {
setPokemons({ pokemons });
localStorage.setItem("pokemons", JSON.stringify(pokemons));
});
}
}, [])
}

Unable to fetch data from api in component

I am working on this project in React JS where I fetch data from this API URL for my frontend development.
I have made my custom hooks to fetch the data into several files following this medium article as follows:
useApiResult.js
import { useState, useEffect } from "react";
export const useApiResult = (request) => {
const [results, setResults] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
fetch(request)
.then(async (response) => {
if (response.ok) {
setResults(await response.json());
setError(null);
} else {
setError(await response.text())
}
})
.catch((err) => {
setError(err.message);
});
}, [request]);
return [results, error];
};
useImages.js
import { useMemo } from "react";
import { useApiResult } from "./useApiResult";
const BASE_URL = "http://api.vidyarajkumari.com";
const createUrl = (base, path) => `${base}${path}`;
const getImages = () => [
createUrl(BASE_URL, "/images/"),
{
method: "GET",
}
];
export const useImages = () => {
const request = useMemo(() => getImages(), []);
return useApiResult(request);
}
React component: Images.js
import React from "react";
import { useImages } from "../../hooks/useImages";
export default function Images() {
const [images, error] = useImages();
//console.log(images);
//console.log(error);
return (
<>
<div className="row">
{
images.map((item, index) => {
<div key={index} className="col-md-4 animate-box">
...
// Rest of code goes here
}
}
</>
</>
)
}
The problem is that I am unable to get the data in the Images.js component from the useImages hook. The console.log values of images return null. This has been bugging me for a while now and I would greatly appreciate a solution to this. What am I doing wrong here and how can I work around this?
P.S. The API Url is live; so feel free to reference it. Thank you for your time.
I Have a better way to do this using useReducer and custom hook, check this:
By the way, I think your API URL has some problems! (I added input for fetching another URL for test)
const IMAGE_URL = "http://api.vidyarajkumari.com/images/";
const initialState = { loading: true };
function fetchReducer(state, action) {
switch (action.type) {
case "fetch":
return {
...state,
error: undefined,
loading: true,
};
case "data":
return {
...state,
data: action.data,
loading: false,
};
case "error":
return {
...state,
error: "Error fetching data. Try again",
loading: false,
};
default:
return state;
}
}
function useFetch(url) {
const [state, dispatch] = React.useReducer(fetchReducer, initialState);
React.useEffect(() => {
dispatch({ type: "fetch" });
fetch(url, {
headers: {
accept: "application/json",
},
})
.then((res) => res.json())
.then((data) => dispatch({ type: "data", data }))
.catch((e) => {
console.warn(e.message);
dispatch({ type: "error" });
});
}, [url]);
return {
loading: state.loading,
data: state.data,
error: state.error,
};
}
function FetchComponent({url}) {
const { loading, data, error } = useFetch(url);
console.log(data);
if (loading) {
return <p>Fetching {url}...</p>;
}
if (error) {
return <p>{error}</p>
}
return <div>{JSON.stringify(data)}</div>
}
const App = () => {
const [url, setUlr] = React.useState(IMAGE_URL)
const inputEl = React.useRef(null);
const changeUrl = () => setUlr(inputEl.current.value)
return (
<React.Fragment>
<input defaultValue="https://icanhazdadjoke.com/" ref={inputEl} type="text" />
<button onClick={changeUrl}>Fetch</button>
{url && <FetchComponent url={url}/>}
</React.Fragment>
)
}
ReactDOM.render(<App/>, document.getElementById("react"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="react"></div>
Give results and error also, in the dependency array, So that component get render when result is updated.
import { useState, useEffect } from "react";
export const useApiResult = (request) => {
const [results, setResults] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
fetch(request)
.then(async (response) => {
if (response.ok) {
setResults(await response.json());
setError(null);
} else {
setError(await response.text())
}
})
.catch((err) => {
setError(err.message);
});
}, [request, results, error]);
return [results, error];
};

Resources