Executing a fetch request using react recoil and react-hook-form

3k views Asked by At

I'm trying to get into using react recoil for state management and I'm loving it so far, however at the moment I'm trying to integrate it with react-hook-form and I can't quite find a setup that works for me.

It's simple, I want to execute a function that uploads a file onSubmit after completing a Form. I've setup a selector to make an async request and update a corresponding atom with the upload progress, but I'm having trouble executing the function as it is getting called within a callback rather than a functional component, I've tried to extract it into a custom hook but that wasn't effective either; I've provided the code below, if anyone has any ideas let me know!

Example error of the current implementation:

React Hook "useRecoilValue" is called in function "submit" which is neither a React function component or a custom React Hook function

Code:


export const videoUploadProgressState = atom({
    key: "videoProgress",
    default: {progress: 0.0}
});

export const videoUploadState = selectorFamily({
    key: 'videoUploadState',
    get: (data : {[key:string] : any}) => async ({get}) => {
        const storage = get(firestoreStorageState);
        const task = storage.ref(data.path).put(data.file);
        const [progress, setProgress] = useRecoilState(videoUploadProgressState)
        task.on("state_changed", snapshot =>{
            switch(snapshot.state){
                case 'running':
                    setProgress({progress: snapshot.bytesTransferred / snapshot.bytesTransferred});
                    break;
                }
            }
        );
        const response = await fetch(
            "http://*********:80/streaming/create-video-asset",
            {
                method:'post', 
                body:JSON.stringify({url: (await task).ref.getDownloadURL()})
            });
        console.log(response.json());
        return response.json();
        
    }
})

const UploadMovieComponent = () => {
  const [title, setTitle] = useState("Select movie");
  const {register, handleSubmit} = useForm();
  const movies = useRecoilValue(movieState);
  const progress = useRecoilValue(videoUploadProgressState)
  const submit = (data) => {
    setTitle(
      useRecoilValue(
        videoUploadState(
          {
            path: `movies/${data.id}/${data.movieFile[0].name}`, 
            file: data.movieFile[0]
          }
        )
      )
    );
  }
  return(
    <div>
      <Form onSubmit={handleSubmit(submit)}>
  <Form.Group controlId="exampleForm.SelectCustom">
    <Form.Label>Select movie</Form.Label>
    <Form.Control as="select" ref={register} name="id" custom>
    {movies.map(movie=> <option value={movie.id}>{movie.title}</option>)}
    </Form.Control>
  </Form.Group>
  <Form.Group>
    <Form.File name="movieFile" ref={register}/>
  </Form.Group>
  <Button type="submit" variant="outline-warning">Upload</Button>
</Form>
<ProgressBar className="mt-2" now={progress.progress} />
    </div>
  );
}

1

There are 1 answers

0
Adrian Coutsoftides On

the solution involved using the set property on the selector to pass in a data object containing the parameters I want to sent to the url.

This way you can use the useSetRecoilState hook to obtain a callback that can be used outside the the scope of the component tree:

/**
* The components
*/

const CineburProgressBar = () => {
  const [progress, setProgress] = useState(0.0);
  const task = useRecoilValue(videoUploadProgressState);
  task?.on("state_changed", (snapshot) => {
    setProgress((snapshot.bytesTransferred / snapshot.totalBytes) * 100);
  });
  return (
    <React.Suspense fallback={<div>Waiting...</div>}>
      <ProgressBar className="mt-2" now={progress} />
    </React.Suspense>
  );
};

const UploadMovieComponent = () => {
  const { register, handleSubmit } = useForm();
  const [result, setResult] = useState("");
  const movies = useRecoilValue(movieState);
  const setRequestData = useSetRecoilState(videoUploadState);

  return (
    <div>
      <Form
        onSubmit={handleSubmit((data) =>
          setRequestData({
            id: data.id,
            file: data.movieFile[0],
          })
        )}
      >
        <Form.Group controlId="exampleForm.SelectCustom">
          <Form.Label>Select movie</Form.Label>
          <Form.Control as="select" ref={register} name="id" custom>
            {movies.map((movie) => (
              <option value={movie.id}>{movie.title}</option>
            ))}
          </Form.Control>
        </Form.Group>
        <Form.Group>
          <Form.File name="movieFile" ref={register} />
        </Form.Group>
        <Button type="submit" variant="outline-warning">
          Upload
        </Button>
      </Form>
      <CineburProgressBar />
    </div>
  );
};

/**
* Firestore setup
*/

firebase.initializeApp(firebaseConfig);

export const firestoreState = atom({
  key: "firestore", // unique ID (with respect to other atoms/selectors)
  default: firebase.app().firestore(), // default value (aka initial value)
});

export const firestoreStorageState = atom({
  key: "firestoreStorage",
  default: firebase.storage(),
});

/**
* Recoil states
*/

export const movieState = selector({
  key: "movieState",
  get: async ({ get }) => {
    const firestore = get(firestoreState);
    const movies = await firestore.collection("movies").get();
    const movieNames = [...movies.docs].map((doc) => {
      const movieData = doc.data();
      return {
        id: doc.id,
        title: movieData.title,
        assetRef: movieData.assetRef,
      };
    });
    return movieNames;
  },
});

/**
 * Monitor video uploading
 */

export const videoUploadProgressState = atom<firebase.storage.UploadTask>({
  key: "videoProgress",
  default: null,
});

export const videoUploadState = selector({
  key: "videoUploadState",
  get: async ({ get }) => {
    return "";
  },
  set: async ({ set, get }, data: { [key: string]: any }) => {
    const ref = `movies/${data.id}/video/${data.file.name}`;
    const storage = get(firestoreStorageState);
    const task = storage.ref(ref).put(data.file);
    set(videoUploadProgressState, task);
    const upload = await task;
    const response = await fetch(
      "http://*******:**/streaming/create-video-asset",
      {
        method: "post",
        headers: {
          "Content-type": "application/json",
        },
        body: JSON.stringify({ url: await upload.ref.getDownloadURL() }),
      }
    );
    const firestore = get(firestoreState);

    const movieDoc = firestore.doc(`movies/${data.id}`);
    const result = await response.json();
    movieDoc.update({ assetRef: result.playback_id });
    return result.playback_id;
  },
});