Skip to content

Commit

Permalink
Merge pull request #330 from bento-platform/refact/contexts
Browse files Browse the repository at this point in the history
refact(explorer): use context instead of redux for explorer individual info
  • Loading branch information
davidlougheed authored Nov 30, 2023
2 parents a3674f6 + 364f083 commit c7d6ba1
Show file tree
Hide file tree
Showing 12 changed files with 86 additions and 105 deletions.
102 changes: 47 additions & 55 deletions src/components/explorer/ExplorerIndividualContent.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import React, { useEffect, useMemo, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { Redirect, Route, Switch, useHistory, useLocation, useParams, useRouteMatch } from "react-router-dom";

Expand All @@ -9,6 +9,8 @@ import { fetchIndividualIfNecessary } from "../../modules/metadata/actions";
import { LAYOUT_CONTENT_STYLE } from "../../styles/layoutContent";
import { matchingMenuKeys, renderMenuItem } from "../../utils/menu";
import { urlPath } from "../../utils/url";

import { ExplorerIndividualContext } from "./contexts/individual";
import { useIsDataEmpty, useDeduplicatedIndividualBiosamples, useIndividualResources } from "./utils";

import SitePageHeader from "../SitePageHeader";
Expand All @@ -21,7 +23,6 @@ import IndividualOntologies from "./IndividualOntologies";
import IndividualTracks from "./IndividualTracks";
import IndividualPhenopackets from "./IndividualPhenopackets";
import IndividualInterpretations from "./IndividualInterpretations";
import { setIndividualId, setIndividualResourcesTuple } from "../../modules/explorer/actions";
import IndividualMedicalActions from "./IndividualMedicalActions";
import IndividualMeasurements from "./IndividualMeasurements";

Expand Down Expand Up @@ -57,12 +58,6 @@ const ExplorerIndividualContent = () => {
}
}, [location]);

useEffect(() => {
if (individualID) {
dispatch(setIndividualId(individualID));
}
}, [dispatch, individualID]);

const metadataService = useSelector((state) => state.services.metadataService);
const individuals = useSelector((state) => state.individuals.itemsByID);

Expand All @@ -76,13 +71,8 @@ const ExplorerIndividualContent = () => {

const { isFetching: individualIsFetching, data: individual } = individuals[individualID] ?? {};

const resources = useIndividualResources(individual);
useEffect(() => {
// Set individual resources in the store for OntologyTerm rendering with no prop drilling
if (resources) {
dispatch(setIndividualResourcesTuple(resources));
}
}, [dispatch, resources]);
const resourcesTuple = useIndividualResources(individual);
const individualContext = useMemo(() => ({ individualID, resourcesTuple }), [individualID, resourcesTuple]);

const overviewUrl = `${individualUrl}/overview`;
const phenotypicFeaturesUrl = `${individualUrl}/phenotypic-features`;
Expand Down Expand Up @@ -163,46 +153,48 @@ const ExplorerIndividualContent = () => {
/>
<Layout>
<Layout.Content style={LAYOUT_CONTENT_STYLE}>
{(individual && !individualIsFetching) ? <Switch>
{/* OVERVIEW */}
<Route path={overviewUrl.replace(":", "\\:")}>
<IndividualOverview individual={individual} />
</Route>
{/* BIOSAMPLES RELATED */}
<Route path={biosamplesUrl.replace(":", "\\:")}>
<IndividualBiosamples individual={individual} experimentsUrl={experimentsUrl}/>
</Route>
<Route path={measurementsUrl.replace(":", "\\:")}>
<IndividualMeasurements individual={individual} />
</Route>
<Route path={phenotypicFeaturesUrl.replace(":", "\\:")}>
<IndividualPhenotypicFeatures individual={individual} />
</Route>
<Route path={diseasesUrl.replace(":", "\\:")}>
<IndividualDiseases individual={individual} />
</Route>
<Route path={interpretationsUrl.replace(":", "\\:")}>
<IndividualInterpretations individual={individual} />
</Route>
<Route path={medicalActionsUrl.replace(":", "\\:")}>
<IndividualMedicalActions individual={individual}/>
</Route>
{/* EXPERIMENTS RELATED*/}
<Route path={experimentsUrl.replace(":", "\\:")}>
<IndividualExperiments individual={individual} />
</Route>
<Route path={tracksUrl.replace(":", "\\:")}>
<IndividualTracks individual={individual} />
</Route>
{/* EXTRA */}
<Route path={ontologiesUrl.replace(":", "\\:")}>
<IndividualOntologies individual={individual} />
</Route>
<Route path={phenopacketsUrl.replace(":", "\\:")}>
<IndividualPhenopackets individual={individual} />
</Route>
<Redirect to={overviewUrl.replace(":", "\\:")} />
</Switch> : <Skeleton loading={true} />}
<ExplorerIndividualContext.Provider value={individualContext}>
{(individual && !individualIsFetching) ? <Switch>
{/* OVERVIEW */}
<Route path={overviewUrl.replace(":", "\\:")}>
<IndividualOverview individual={individual} />
</Route>
{/* BIOSAMPLES RELATED */}
<Route path={biosamplesUrl.replace(":", "\\:")}>
<IndividualBiosamples individual={individual} experimentsUrl={experimentsUrl}/>
</Route>
<Route path={measurementsUrl.replace(":", "\\:")}>
<IndividualMeasurements individual={individual} />
</Route>
<Route path={phenotypicFeaturesUrl.replace(":", "\\:")}>
<IndividualPhenotypicFeatures individual={individual} />
</Route>
<Route path={diseasesUrl.replace(":", "\\:")}>
<IndividualDiseases individual={individual} />
</Route>
<Route path={interpretationsUrl.replace(":", "\\:")}>
<IndividualInterpretations individual={individual} />
</Route>
<Route path={medicalActionsUrl.replace(":", "\\:")}>
<IndividualMedicalActions individual={individual}/>
</Route>
{/* EXPERIMENTS RELATED*/}
<Route path={experimentsUrl.replace(":", "\\:")}>
<IndividualExperiments individual={individual} />
</Route>
<Route path={tracksUrl.replace(":", "\\:")}>
<IndividualTracks individual={individual} />
</Route>
{/* EXTRA */}
<Route path={ontologiesUrl.replace(":", "\\:")}>
<IndividualOntologies individual={individual} />
</Route>
<Route path={phenopacketsUrl.replace(":", "\\:")}>
<IndividualPhenopackets individual={individual} />
</Route>
<Redirect to={overviewUrl.replace(":", "\\:")} />
</Switch> : <Skeleton loading={true} />}
</ExplorerIndividualContext.Provider>
</Layout.Content>
</Layout>
</>;
Expand Down
16 changes: 10 additions & 6 deletions src/components/explorer/IndividualVariants.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import React, { useMemo } from "react";
import React, { useContext, useMemo } from "react";
import { Link } from "react-router-dom";
import { useDispatch, useSelector } from "react-redux";
import { useDispatch } from "react-redux";
import PropTypes from "prop-types";

import { Button, Descriptions } from "antd";

import { setIgvPosition } from "../../modules/explorer/actions";

import "./explorer.css";
import { ExplorerIndividualContext } from "./contexts/individual";
import { explorerIndividualUrl } from "./utils";

import JsonView from "./JsonView";
import OntologyTerm from "./OntologyTerm";
import { GeneDescriptor } from "./IndividualGenes";
Expand All @@ -24,12 +28,12 @@ const variantExpressionPropType = PropTypes.shape({

const VariantExpressionDetails = ({variantExpression, geneContext}) => {
const dispatch = useDispatch();
const individualId = useSelector(state => state.explorer.individualId);
const { individualID } = useContext(ExplorerIndividualContext);
const tracksUrl = useMemo(() => {
if (individualId) {
return `/data/explorer/individuals/${individualId}/tracks`;
if (individualID) {
return `${explorerIndividualUrl(individualID)}/tracks`;
}
}, [individualId]);
}, [individualID]);
return (
<div style={variantStyle}>
<span style={{display: "inline"}}>
Expand Down
6 changes: 3 additions & 3 deletions src/components/explorer/OntologyTerm.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { memo, useEffect } from "react";
import React, { memo, useContext, useEffect } from "react";
import PropTypes from "prop-types";

import { Button, Icon } from "antd";
Expand All @@ -7,8 +7,8 @@ import { EM_DASH } from "../../constants";
import { ontologyShape } from "../../propTypes";
import { id } from "../../utils/misc";

import { ExplorerIndividualContext } from "./contexts/individual";
import { useResourcesByNamespacePrefix } from "./utils";
import { useSelector } from "react-redux";

export const conditionalOntologyRender = (field) => (_, record) => {
if (record.hasOwnProperty(field)) {
Expand All @@ -19,7 +19,7 @@ export const conditionalOntologyRender = (field) => (_, record) => {
};

const OntologyTerm = memo(({ term, renderLabel, br }) => {
const resourcesTuple = useSelector(state => state.explorer.individualResourcesTuple);
const { resourcesTuple } = useContext(ExplorerIndividualContext);

// TODO: perf: might be slow to generate this over and over
const [resourcesByNamespacePrefix, isFetchingResources] = useResourcesByNamespacePrefix(resourcesTuple);
Expand Down
3 changes: 3 additions & 0 deletions src/components/explorer/contexts/individual.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { createContext } from "react";

export const ExplorerIndividualContext = createContext({ individualID: null, resourcesTuple: [[], false] });
19 changes: 11 additions & 8 deletions src/components/explorer/searchResultsTables/BiosampleIDCell.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
import React from "react";
import React, { useContext } from "react";
import { Link, useLocation } from "react-router-dom";
import PropTypes from "prop-types";
import { useSelector } from "react-redux";

import { ExplorerIndividualContext } from "../contexts/individual";
import { explorerIndividualUrl } from "../utils";

/**
* A Link to the provided biosample in the explorer.
* If no individualId prop is provided, the link will use the current individual ID in the explorer's state.
* If no individualID prop is provided, the link will use the current individual ID in the explorer's state.
* @param {string} biosample biosample ID to link to
* @param {string} individualId (optional) individual ID to link to
* @param {string} individualID (optional) individual ID to link to
*/
const BiosampleIDCell = React.memo(({ biosample, individualId }) => {
const BiosampleIDCell = React.memo(({ biosample, individualID }) => {
const location = useLocation();
const usedIndividualId = individualId ? individualId : useSelector(state => state.explorer?.individualId ?? "");
const { individualID: contextIndividualID } = useContext(ExplorerIndividualContext);
const usedIndividualID = individualID ?? contextIndividualID;
return (
<Link
to={{
pathname: `/data/explorer/individuals/${usedIndividualId}/biosamples/${biosample}`,
pathname: `${explorerIndividualUrl(usedIndividualID)}/biosamples/${biosample}`,
state: { backUrl: location.pathname },
}}
>
Expand All @@ -26,7 +29,7 @@ const BiosampleIDCell = React.memo(({ biosample, individualId }) => {

BiosampleIDCell.propTypes = {
biosample: PropTypes.string.isRequired,
individualId: PropTypes.string,
individualID: PropTypes.string,
};

export default BiosampleIDCell;
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ const BIOSAMPLES_COLUMNS = [
title: "Biosample",
dataIndex: "biosample",
render: (biosample, { individual }) => (
<BiosampleIDCell biosample={biosample} individualId={individual.id} />
<BiosampleIDCell biosample={biosample} individualID={individual.id} />
),
sorter: (a, b) => a.biosample.localeCompare(b.biosample),
defaultSortOrder: "ascend",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useSelector } from "react-redux";
import PropTypes from "prop-types";

import { useSortedColumns } from "../hooks/explorerHooks";
import { explorerIndividualUrl } from "../utils";

import BiosampleIDCell from "./BiosampleIDCell";
import ExplorerSearchResultsTable from "../ExplorerSearchResultsTable";
Expand All @@ -14,7 +15,7 @@ const ExperimentRender = React.memo(({ experimentId, individual }) => {
<>
<Link
to={{
pathname: `/data/explorer/individuals/${individual.id}/experiments/${experimentId}`,
pathname: `${explorerIndividualUrl(individual.id)}/experiments/${experimentId}`,
state: { backUrl: location.pathname },
}}
>
Expand Down Expand Up @@ -50,7 +51,7 @@ const SEARCH_RESULT_COLUMNS_EXP = [
title: "Biosample",
dataIndex: "biosampleId",
render: (biosampleId, record) => (
<BiosampleIDCell biosample={biosampleId} individualId={record.individual.id} />
<BiosampleIDCell biosample={biosampleId} individualID={record.individual.id} />
),
sorter: (a, b) => a.biosampleId.localeCompare(b.biosampleId),
sortDirections: ["descend", "ascend", "descend"],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from "react";
import {Link, useLocation} from "react-router-dom";
import PropTypes from "prop-types";
import { explorerIndividualUrl } from "../utils";

const IndividualIDCell = React.memo(({individual: {id, alternate_ids: alternateIds}}) => {
const location = useLocation();
Expand All @@ -9,7 +10,7 @@ const IndividualIDCell = React.memo(({individual: {id, alternate_ids: alternateI
<>
<Link
to={{
pathname: `/data/explorer/individuals/${id}/overview`,
pathname: `${explorerIndividualUrl(id)}/overview`,
state: { backUrl: location.pathname },
}}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ const SEARCH_RESULT_COLUMNS = [
{
title: "Samples",
dataIndex: "biosamples",
render: (samples, {individual: {id: individualId}}) => (
render: (samples, {individual: {id: individualID}}) => (
<>
{samples.length} Sample{samples.length === 1 ? "" : "s"}
{samples.length ? ": " : ""}
{samples.map((s, si) => <React.Fragment key={s}>
<BiosampleIDCell biosample={s} individualId={individualId} />
<BiosampleIDCell biosample={s} individualID={individualID} />
{si < samples.length - 1 ? ", " : ""}
</React.Fragment>)}
</>
Expand Down
3 changes: 3 additions & 0 deletions src/components/explorer/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ const useGenomicInterpretationsWithCall = (interpretations, call) =>
.map(gi => [gi.subject_or_biosample_id, gi]),
),
),
[interpretations, call],
);

export const useIndividualVariantInterpretations = (individual) => {
Expand Down Expand Up @@ -144,3 +145,5 @@ export const ontologyTermSorter = (k) => (a, b) => {
}
return 0;
};

export const explorerIndividualUrl = (individualID) => `/data/explorer/individuals/${individualID}`;
14 changes: 1 addition & 13 deletions src/modules/explorer/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ export const SET_TABLE_SORT_ORDER = "EXPLORER.SET_TABLE_SORT_ORDER";
export const RESET_TABLE_SORT_ORDER = "EXPLORER.RESET_TABLE_SORT_ORDER";
export const SET_ACTIVE_TAB = "EXPLORER.SET_ACTIVE_TAB";
export const SET_IGV_POSITION = "EXPLORER.SET_IGV_POSITION";
export const SET_INDIVIDUAL_ID = "EXPLORER.SET_INDIVIDUAL_ID";
export const SET_INDIVIDUAL_RESOURCES_TUPLE = "EXPLORER.SET_RESOURCES_TUPLE";

const performSearch = networkAction((datasetID, dataTypeQueries, excludeFromAutoJoin = []) => (dispatch, getState) => ({
types: PERFORM_SEARCH,
Expand Down Expand Up @@ -177,7 +175,7 @@ export const neutralizeAutoQueryPageTransition = () => ({
// search unpaginated for now, since that's how standard queries are currently handled
const performFreeTextSearch = networkAction(
(datasetID, term) => (dispatch, getState) => (
console.log("performFreeTextSearch", datasetID, term),
console.log("performFreeTextSearch", datasetID, term) ||
{
types: FREE_TEXT_SEARCH,
params: { datasetID },
Expand All @@ -203,16 +201,6 @@ export const setIgvPosition = (igvPosition) => ({
igvPosition,
});

export const setIndividualId = (id) => ({
type: SET_INDIVIDUAL_ID,
id,
});

export const setIndividualResourcesTuple = (tuples) => ({
type: SET_INDIVIDUAL_RESOURCES_TUPLE,
resourcesTuple: tuples,
});

export const performGetGohanVariantsOverviewIfPossible = () => (dispatch, getState) => {
const gohanUrl = getState()?.services?.itemsByKind?.gohan?.url;
if (!gohanUrl) return;
Expand Down
14 changes: 0 additions & 14 deletions src/modules/explorer/reducers.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,6 @@ import {
FREE_TEXT_SEARCH,
SET_OTHER_THRESHOLD_PERCENTAGE,
SET_IGV_POSITION,
SET_INDIVIDUAL_ID,
SET_INDIVIDUAL_RESOURCES_TUPLE,
} from "./actions";

// TODO: Could this somehow be combined with discovery?
Expand All @@ -55,8 +53,6 @@ export const explorer = (
otherThresholdPercentage:
readFromLocalStorage("otherThresholdPercentage") ?? DEFAULT_OTHER_THRESHOLD_PERCENTAGE,
igvPosition: undefined,
individualId: "",
individualResourcesTuple: [],
},
action,
) => {
Expand Down Expand Up @@ -311,16 +307,6 @@ export const explorer = (
...state,
igvPosition: action.igvPosition,
};
case SET_INDIVIDUAL_ID:
return {
...state,
individualId: action.id,
};
case SET_INDIVIDUAL_RESOURCES_TUPLE:
return {
...state,
individualResourcesTuple: action.resourcesTuple,
};
default:
return state;
}
Expand Down

0 comments on commit c7d6ba1

Please sign in to comment.