function useCustomCharts({ storage = true } = { storage: true }) {
  const [customCharts, setCustomCharts] = useState([])

  // Loads custom charts saved in user storage
  useEffect(() => {
    if (storage) {
      loadStoredCustomCharts().then(setCustomCharts)
    }
  }, [storage])

  const loadCustomChartsFromUrlAsSource = useCallback(
    async (source, url) => {
      let newChartsToInject = await requireRawChartsFromUrl(url)
      if (newChartsToInject.length === 0) {
        return
      }
      newChartsToInject = newChartsToInject.map((chart) => ({
        ...chart,
        rawCustomChart: {
          source,
          url,
        },
      }))
      const [
        nextCustomCharts,
        releasedCustomCharts,
      ] = getNextCustomChartsAndReleased(customCharts, newChartsToInject)
      releasedCustomCharts.forEach((c) => {
        URL.revokeObjectURL(c.rawCustomChart.url)
      })
      setCustomCharts(nextCustomCharts)
      if (storage) {
        await storeCustomCharts(nextCustomCharts)
      }
      return nextCustomCharts
    },
    [customCharts, storage]
  )

  const loadCustomChartsFromUrl = useCallback(
    async (url) => {
      const source = `url:${url}`
      return loadCustomChartsFromUrlAsSource(source, url)
    },
    [loadCustomChartsFromUrlAsSource]
  )

  const loadCustomChartsFromNpm = useCallback(
    async (name) => {
      const source = `npm:${name}`
      const url = NPM_CDN + name
      return loadCustomChartsFromUrlAsSource(source, url)
    },
    [loadCustomChartsFromUrlAsSource]
  )

  const uploadCustomCharts = useCallback(
    async (file, mode = 'add') => {
      if (!file) {
        return []
      }
      const url = URL.createObjectURL(file)
      let newChartsToInject = await requireRawChartsFromUrl(url)
      if (newChartsToInject.length === 0) {
        return
      }
      const fileHash = await makeFileHash(file)
      const source = `file:${fileHash}`
      newChartsToInject = newChartsToInject.map((chart) => ({
        ...chart,
        rawCustomChart: {
          source,
          url,
        },
      }))
      const [nextCustomCharts, releasedCustomCharts] =
        mode === 'replace'
          ? [newChartsToInject, customCharts]
          : getNextCustomChartsAndReleased(customCharts, newChartsToInject)
      releasedCustomCharts.forEach((c) => {
        URL.revokeObjectURL(c.rawCustomChart.url)
      })
      setCustomCharts(nextCustomCharts)
      if (storage) {
        const cache = await window.caches.open(STORE_NS)
        await cache.put(fileHash, new Response(file))
        await storeCustomCharts(nextCustomCharts)
      }
      return nextCustomCharts
    },
    [customCharts, storage]
  )

  const importCustomChartFromProject = useCallback(
    async (projectChart) => {
      const { source, content } = projectChart.rawCustomChart
      let url, file, fileHash
      if (source.indexOf('url:') === 0) {
        url = source.replace('url:', '')
      } else if (source.indexOf('npm:') === 0) {
        url = NPM_CDN + source.replace('npm:', '')
      } else if (source.indexOf('file:') === 0) {
        fileHash = source.replace('file:', '')
        file = new File([content], `${fileHash}.js`, {
          type: 'application/json',
        })
        url = URL.createObjectURL(file)
      } else {
        throw new Error(`Try to import invalid source ${source}`)
      }
      const newChartsToInject = await requireRawChartsFromUrl(url)
      let newChart = find(
        newChartsToInject,
        (c) => c.metadata.id === projectChart.metadata.id
      )
      if (!newChart) {
        throw new Error(
          `Can't find chart ${projectChart.metadata.id} from ${source}`
        )
      }
      newChart = {
        ...newChart,
        rawCustomChart: {
          source,
          url,
        },
      }
      const [
        nextCustomCharts,
        releasedCustomCharts,
      ] = getNextCustomChartsAndReleased(customCharts, [newChart])
      releasedCustomCharts.forEach((c) => {
        URL.revokeObjectURL(c.rawCustomChart.url)
      })
      setCustomCharts(nextCustomCharts)
      if (storage) {
        if (file) {
          const cache = await window.caches.open(STORE_NS)
          await cache.put(fileHash, new Response(file))
        }
        await storeCustomCharts(nextCustomCharts)
      }
      return newChart
    },
    [customCharts, storage]
  )

  const removeCustomChart = useCallback(
    async (chart) => {
      const nextCustomCharts = customCharts.filter(
        (c) => c.metadata.id !== chart.metadata.id
      )
      setCustomCharts(nextCustomCharts)
      if (storage) {
        await storeCustomCharts(nextCustomCharts)
      }
      return nextCustomCharts
    },
    [customCharts, storage]
  )

  return [
    customCharts,
    {
      uploadCustomCharts,
      removeCustomChart,
      loadCustomChartsFromUrl,
      loadCustomChartsFromNpm,
      exportCustomChart,
      importCustomChartFromProject,
    },
  ]
}
This hook handle the custom user charts.
It's business is to load custom charts from some `source`:
 - `file:hash` File source we hash the file content to identify "which" file
 - `url:cdn_url` An url that point to a js file bundled as UMD | AMD
 - `npm:name` A valid package name on npm registry bundled also as UMD | AMD

It loads the current custom charts on mount from user storage and sync
them when you call its methods.

@param {{ storage: boolean }}
@returns {[CustomChartContract[], {
 uploadCustomCharts: (file?: File, mode?:  'replace' | 'add') => Promise<CustomChartContract[]>
 loadCustomChartsFromUrl: (url: string) => Promise<CustomChartContract[]>
 loadCustomChartsFromNpm: (name: string) => Promise<CustomChartContract[]>
 importCustomChartFromProject: (projectChart: CustomChartContract) => Promise<CustomChartContract>
 removeCustomChart: (chart: CustomChartContract) => Promise<CustomChartContract[]>
 exportCustomChart: (chart: CustomChartContract) => Promise<{ source: string, content: string | null }>
}]}