CSFrequency / react-firebase-hooks

React Hooks for Firebase.
Apache License 2.0
3.59k stars 305 forks source link

Add or update to firebase #2

Closed LiteSoul closed 5 years ago

LiteSoul commented 5 years ago

Unless I'm missing something, besides listening to the firebase DB, how can we send or update data to it with some custom hook?

chrisbianca commented 5 years ago

You're correct, the hooks purely handle listening to the firebase DB.

You don't need hooks to be able to send or update data, you can simply use the standard Firebase APIs, e.g.

const Component = () => {
  const ref = firebase.database().ref('/example');
  const { loading, value } = useObjectVal(ref);

  const setValue = (v) => ref.update(v);

  return (
    <div>
      <div>Current value: {value}</div>
      <button onClick={() => setValue('Changed')}>Update</button>
   </div>
  );
}
NixBiks commented 5 years ago

I can't use const { loading, value } = useObjectVal(ref);

Argument of type 'DocumentReference' is not assignable to parameter of type 'Query'.

brettinternet commented 4 years ago

Here's something that uses a variation of the pattern presented in this library. See useAsync at the bottom.

useAsync.ts

/**
 * Similar to `useLoadingValue.ts` (see link), but loading isn't true by default when value is `undefined`
 * https://github.com/CSFrequency/react-firebase-hooks/blob/7eb49f1624d7c1bfb5ad8083a8702b19bf0e6929/util/useLoadingValue.ts
 */
import { useReducer, useCallback } from 'react'

export type LoadableHook<V, T, E> = [
  { value?: V; executor: (arg: T) => void; reset: () => void },
  boolean,
  E | undefined
]

export type LoadableValue<T, E> = {
  error?: E
  loading: boolean
  reset: () => void
  setLoading: (loading: boolean) => void
  setError: (error: E) => void
  setValue: (value?: T | null) => void
  value?: T
}

type ReducerState<E> = {
  error?: E
  loading: boolean
  value?: any
}

type ErrorAction<E> = { type: 'error'; error: E }
type ResetAction = { type: 'reset'; defaultValue?: any }
type ValueAction = { type: 'value'; value: any }
type LoadingAction = { type: 'loading'; loading: boolean }
type ReducerAction<E> =
  | ErrorAction<E>
  | ResetAction
  | ValueAction
  | LoadingAction

const defaultState = (defaultValue?: any) => {
  return {
    loading: false,
    value: defaultValue,
  }
}

const reducer = <E>() => (
  state: ReducerState<E>,
  action: ReducerAction<E>
): ReducerState<E> => {
  switch (action.type) {
    case 'loading':
      return {
        ...state,
        loading: action.loading,
      }
    case 'error':
      return {
        ...state,
        error: action.error,
        loading: false,
        value: undefined,
      }
    case 'reset':
      return defaultState(action.defaultValue)
    case 'value':
      return {
        ...state,
        error: undefined,
        loading: false,
        value: action.value,
      }
    default:
      return state
  }
}

export const useLoadableValue = <T, E>(
  getDefaultValue?: () => T | null
): LoadableValue<T, E> => {
  const defaultValue = getDefaultValue ? getDefaultValue() : undefined
  const [state, dispatch] = useReducer(reducer<E>(), defaultState(defaultValue))

  const reset = () => {
    const defaultValue = getDefaultValue ? getDefaultValue() : undefined
    dispatch({ type: 'reset', defaultValue })
  }

  const setLoading = (loading: boolean) => {
    dispatch({ type: 'loading', loading })
  }

  const setError = (error: E) => {
    dispatch({ type: 'error', error })
  }

  const setValue = (value?: T | null) => {
    dispatch({ type: 'value', value })
  }

  return {
    error: state.error,
    loading: state.loading,
    reset,
    setLoading,
    setError,
    setValue,
    value: state.value,
  }
}

export type AsyncHookNoValue<T> = [(arg: T) => void, boolean, Error | undefined]

export type AsyncHook<V, T> = [
  { value?: V; executor: (arg: T) => void; reset?: () => void },
  boolean,
  Error | undefined
]

export const useAsync = <V, T>(
  asyncFunction: (arg: T) => Promise<V>
): LoadableHook<V, T, Error> => {
  const {
    value,
    loading,
    error,
    reset,
    setLoading,
    setError,
    setValue,
  } = useLoadableValue<V, Error>()

  const executor = useCallback(
    async (arg: T) => {
      setLoading(true)
      try {
        const value = (await asyncFunction(arg)) as V
        setValue(value)
        setLoading(false)
      } catch (error) {
        setError(error)
      }
    },
    [asyncFunction] // eslint-disable-line react-hooks/exhaustive-deps
  )

  return [{ value, executor, reset }, loading, error]
}

And then here's how you use it:

createData.ts

const getDataCreator = () => {
const firebaseContext = useContext(FirebaseContext) // eslint-disable-line react-hooks/rules-of-hooks
  if (firebaseContext) {
    const dataCollection = firebaseContext.firestore().collection('some-collection')
    return (title: string) =>
    dataCollection!?.add({
      title,
      createdDate: firebase.firestore.FieldValue.serverTimestamp(),
    })
  }
}

export const useDataCreator = (): AsyncHook<
  firebase.firestore.DocumentReference<firebase.firestore.DocumentData>,
  string
> => {
  const createData = getDataCreator()
  let [{ value, executor, reset }, isLoading, error] = useAsync(createData)

  if (error) {
    logger.error(error)
    error = Error('Unable to create data')
  }

  return [{ value, executor, reset }, isLoading, error]
}