Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#1238] Create a derived atom for dataset hydration from Next.js #1266

Merged
merged 2 commits into from
Nov 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions app/scripts/components/exploration/atoms/datasetLayers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { atom } from 'jotai';
import { DatasetLayer } from '$types/veda';

/**
* This is the primary storage atom for external datasets (e.g. passed from Next.js).
*/
export const externalDatasetsAtom = atom<any[]>([]);

/**
* Derived atom that transforms the provided datasets into layers.
* It is used by the timelineDatasetsAtom to rebuild state from URL parameters
* while it preserves the parent dataset metadata for each layer that comes
* from the MDX configuration.
*/
export const datasetLayersAtom = atom<DatasetLayer[]>((get) => {
const datasets = get(externalDatasetsAtom);

return datasets.flatMap((dataset) => {
return (dataset.layers || []).map((l: any) => ({
...l,
parentDataset: {
id: dataset.id,
name: dataset.name
}
}));
});
});
12 changes: 8 additions & 4 deletions app/scripts/components/exploration/atoms/datasets.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { datasetLayers } from '../data-utils';
import { reconcileDatasets } from '../data-utils-no-faux-module';
import { TimelineDataset, TimelineDatasetForUrl } from '../types.d.ts';
import { datasetLayersAtom } from './datasetLayers';
import { atomWithUrlValueStability } from '$utils/params-location-atom/atom-with-url-value-stability';

function urlDatasetsDehydrate(datasets: TimelineDataset[]) {
Expand Down Expand Up @@ -38,7 +38,7 @@ export const timelineDatasetsAtom = atomWithUrlValueStability<
dehydrate: (datasets) => {
return urlDatasetsDehydrate(datasets);
},
reconcile: (urlDatasets, storageDatasets) => {
reconcile: (urlDatasets, storageDatasets, get) => {
// Reconcile what needs to be reconciled.
const reconciledDatasets = urlDatasets.map((enc) => {
// We only want to do this on load. If the dataset was already
Expand All @@ -49,10 +49,14 @@ export const timelineDatasetsAtom = atomWithUrlValueStability<
if (readyDataset) {
return readyDataset;
}
const currentDatasetLayers = get(datasetLayersAtom);
// Reconcile the dataset with the internal data (from VEDA config files)
// and then add the url stored settings.
// @TODO - replace datasetLayers
const [reconciled] = reconcileDatasets([enc.id], datasetLayers, []);
const [reconciled] = reconcileDatasets(
[enc.id],
currentDatasetLayers,
[]
);
if (enc.settings) {
reconciled.settings = enc.settings;
}
Expand Down
9 changes: 6 additions & 3 deletions app/scripts/components/exploration/container.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import React, { useState } from 'react';
import { TourProvider } from '@reactour/tour';
import { DevTools } from 'jotai-devtools';
import { useAtom } from 'jotai';
import { useAtom, useSetAtom } from 'jotai';
import { PopoverTourComponent, TourManager } from './tour-manager';

import { DatasetSelectorModal } from './components/dataset-selector-modal';
import { allExploreDatasets } from './data-utils';
import useTimelineDatasetAtom from './hooks/use-timeline-dataset-atom';
import { externalDatasetsAtom } from './atoms/datasetLayers';
import ExplorationAndAnalysis from '.';
import { urlAtom } from '$utils/params-location-atom/url';
import { DATASETS_PATH, EXPLORATION_PATH } from '$utils/routes';
Expand All @@ -32,6 +33,8 @@ const tourProviderStyles = {
};

export default function ExplorationAndAnalysisContainer() {
const setExternalDatasets = useSetAtom(externalDatasetsAtom);
setExternalDatasets(allExploreDatasets);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current approach using setExternalDatasets works, but it might need to be called in multiple places where we depend on the static datasets.

I was wondering if a more scalable solution would be to use our existing EnvConfigProvider and evolve it into a more general ConfigProvider. Like this it would centralize both our env and data configuration in one place.

The provider could be used like this (note the extra datasets prop passed):

<EnvConfigProvider
      config={{
                envMapboxToken: process.env.NEXT_PUBLIC_MAPBOX_TOKEN ?? '',
                envApiStacEndpoint: process.env.NEXT_PUBLIC_API_STAC_ENDPOINT ?? '',
                envApiRasterEndpoint: process.env.NEXT_PUBLIC_API_RASTER_ENDPOINT ?? '',
                datasets: static-datasets
      }}
>
              {children}
</EnvConfigProvider>

And internally, the provider would handle dataset hydration:

const setExternalDatasets = useSetAtom(externalDatasetsAtom);
setExternalDatasets(allExploreDatasets);

Like that the setter won't have to be manually called several times if it's needed.

Copy link
Collaborator

@hanbyul-here hanbyul-here Nov 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How you described makes sense. My concern for now is how nicely it would work out irl considering Jotai positions itself as a replacement for context (I am reading https://jotai.org/docs/basics/comparison) - or it just means that we are really not taking full advantage of Jotai. I will put more thoughts into this from the perspective of global state management.

const [timelineDatasets, setTimelineDatasets] = useTimelineDatasetAtom();
const [datasetModalRevealed, setDatasetModalRevealed] = useState(
!timelineDatasets.length
Expand All @@ -40,8 +43,8 @@ export default function ExplorationAndAnalysisContainer() {
// @NOTE: When Exploration page is preloaded (ex. Linked with react-router)
// atomWithLocation gets initialized outside of Exploration page and returns the previous page's value
// We check if url Atom actually returns the values for exploration page here.
const [currentUrl]= useAtom(urlAtom);
if(!currentUrl.pathname?.includes(EXPLORATION_PATH)) return null;
const [currentUrl] = useAtom(urlAtom);
if (!currentUrl.pathname?.includes(EXPLORATION_PATH)) return null;

const openModal = () => setDatasetModalRevealed(true);
const closeModal = () => setDatasetModalRevealed(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import { useAtom } from 'jotai';
import { timelineDatasetsAtom } from '../atoms/datasets';
import { TimelineDataset } from '../types.d.ts';

export default function useTimelineDatasetAtom (): [
export default function useTimelineDatasetAtom(): [
TimelineDataset[],
(datasets: TimelineDataset[]) => void
] {
] {
const [datasets, setDatasets] = useAtom(timelineDatasetsAtom);
return [datasets, setDatasets];
}
}
8 changes: 7 additions & 1 deletion app/scripts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ import useTimelineDatasetAtom from '$components/exploration/hooks/use-timeline-d
import { timelineDatasetsAtom } from '$components/exploration/atoms/datasets';
import { DatasetSelectorModal } from '$components/exploration/components/dataset-selector-modal';
import { EnvConfigProvider } from '$context/env-config';
import {
datasetLayersAtom,
externalDatasetsAtom
} from '$components/exploration/atoms/datasetLayers';

// Adding .last property to array
/* eslint-disable-next-line fp/no-mutating-methods */
Expand Down Expand Up @@ -77,5 +81,7 @@ export {
InternalNavLink,

// STATE
timelineDatasetsAtom
timelineDatasetsAtom,
externalDatasetsAtom,
datasetLayersAtom
};
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,15 @@ interface StableAtomOptions<Value, ValueUrl> {
* the information that goes to the url needs to be simplified.
* @param urlValue The value stored in the URL parameter after hydration.
* @param storageValue The value stored in the atom.
* @param get A getter function to access other atom values during reconciliation.
* @returns The reconciled value. If the function is not provided the urlValue
* is considered the reconciled value.
*/
reconcile?: (urlValue: ValueUrl, storageValue: Value) => Value;
reconcile?: (
urlValue: ValueUrl,
storageValue: Value,
get: (atom: any) => any
) => Value;
/**
* An optional function to compare two atom values for equality.
* @param prev The previous atom value.
Expand Down Expand Up @@ -93,7 +98,7 @@ export function atomWithUrlValueStability<T, TUrl = T>(
const storageValue = get(storage);

// Reconcile the hydrated value with the storage value.
const reconciled = reconcile(hydrated, storageValue);
const reconciled = reconcile(hydrated, storageValue, get);

// If the reconciled value is equal to the storage value, return the
// storage value to ensure equality.
Expand Down
Loading