Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/src/packages/frontend/app-framework/redux-hooks.ts
Views: 687
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45/*67**IMPORTANT:** TYPED REDUX HOOKS -- If you use89useTypedRedux('name' | {project_id:'the project id'}, 'one field')1011then you will get good guaranteed typing (unless, of course, the global store12hasn't been converted to typescript yet!). If you use plain useRedux, you13get a dangerous "any" type out!1415---1617Hook for getting anything from our global redux store, and this should18also work fine with computed properties.1920Use it is as follows:2122With a named store, such as "projects", "account", "page", etc.:2324useRedux(['name-of-store', 'path', 'in', 'store'])2526With a specific project:2728useRedux(['path', 'in', 'project store'], 'project-id')2930Or with an editor in a project:3132useRedux(['path', 'in', 'project store'], 'project-id', 'path')3334If you don't know the name of the store initially, you can use a name of '',35and you'll always get back undefined.3637useRedux(['', 'other', 'stuff']) === undefined38*/3940import { is_valid_uuid_string } from "@cocalc/util/misc";41import { redux, ProjectActions, ProjectStore } from "../app-framework";42import { ProjectStoreState } from "../project_store";43import React, { useEffect, useRef } from "react";44import * as types from "./actions-and-stores";45import useDeepCompareEffect from "use-deep-compare-effect";4647export function useReduxNamedStore(path: string[]) {48const [value, set_value] = React.useState(() => {49return redux.getStore(path[0])?.getIn(path.slice(1) as any) as any;50});5152useDeepCompareEffect(() => {53if (path[0] == "") {54// Special case -- we allow passing "" for the name of the store and get out undefined.55// This is useful when using the useRedux hook but when the name of the store isn't known initially.56return undefined;57}58const store = redux.getStore(path[0]);59if (store == null) {60// This could happen if some input is invalid, e.g., trying to create one of these61// redux hooks with an invalid project_id. There will be other warnings in the logs62// about that. It's better at this point to warn once in the logs, rather than completely63// crash the client.64console.warn(`store "${path[0]}" must exist; path=`, path);65return undefined;66}67const subpath = path.slice(1);68let last_value = value;69const f = () => {70if (!f.is_mounted) {71// CRITICAL: even after removing the change listener, sometimes f gets called;72// I don't know why EventEmitter has those semantics, but it definitely does.73// That's why we *also* maintain this is_mounted flag.74return;75}76const new_value = store.getIn(subpath as any);77if (last_value !== new_value) {78/*79console.log("useReduxNamedStore change ", {80name: path[0],81path: JSON.stringify(path),82new_value,83last_value,84});85*/86last_value = new_value;87set_value(new_value);88}89};90f.is_mounted = true;91store.on("change", f);92f();93return () => {94f.is_mounted = false;95store.removeListener("change", f);96};97}, [path]);9899return value;100}101102function useReduxProjectStore(path: string[], project_id: string) {103const [value, set_value] = React.useState(() =>104redux105.getProjectStore(project_id)106.getIn(path as [string, string, string, string, string]),107);108109useDeepCompareEffect(() => {110const store = redux.getProjectStore(project_id);111let last_value = value;112const f = (obj) => {113if (obj == null || !f.is_mounted) return; // see comment for useReduxNamedStore114const new_value = obj.getIn(path);115if (last_value !== new_value) {116/*117console.log("useReduxProjectStore change ", {118path: JSON.stringify(path),119new_value,120last_value,121});122*/123last_value = new_value;124set_value(new_value);125}126};127f.is_mounted = true;128store.on("change", f);129f(store);130return () => {131f.is_mounted = false;132store.removeListener("change", f);133};134}, [path, project_id]);135136return value;137}138139function useReduxEditorStore(140path: string[],141project_id: string,142filename: string,143) {144const [value, set_value] = React.useState(() =>145// the editor itself might not be defined hence the ?. below:146redux147.getEditorStore(project_id, filename)148?.getIn(path as [string, string, string, string, string]),149);150151useDeepCompareEffect(() => {152let store = redux.getEditorStore(project_id, filename);153let last_value = value;154const f = (obj) => {155if (obj == null || !f.is_mounted) return; // see comment for useReduxNamedStore156const new_value = obj.getIn(path);157if (last_value !== new_value) {158last_value = new_value;159set_value(new_value);160}161};162f.is_mounted = true;163f(store);164if (store != null) {165store.on("change", f);166} else {167/* This code is extra complicated since we account for the case168when getEditorStore is undefined then becomes defined.169Very rarely there are components that useRedux and somehow170manage to do so before the editor store gets created.171NOTE: I might be able to solve this same problem with172simpler code with useAsyncEffect...173*/174const g = () => {175if (!f.is_mounted) {176unsubscribe();177return;178}179store = redux.getEditorStore(project_id, filename);180if (store != null) {181unsubscribe();182f(store); // may have missed an initial change183store.on("change", f);184}185};186const unsubscribe = redux.reduxStore.subscribe(g);187}188189return () => {190f.is_mounted = false;191store?.removeListener("change", f);192};193}, [path, project_id, filename]);194195return value;196}197198export interface StoreStates {199account: types.AccountState;200"admin-site-licenses": types.SiteLicensesState;201"admin-users": types.AdminUsersState;202billing: types.BillingState;203compute_images: types.ComputeImagesState;204customize: types.CustomizeState;205file_use: types.FileUseState;206mentions: types.MentionsState;207page: types.PageState;208projects: types.ProjectsState;209users: types.UsersState;210news: types.NewsState;211}212213export function useTypedRedux<214T extends keyof StoreStates,215S extends keyof StoreStates[T],216>(store: T, field: S): StoreStates[T][S];217218export function useTypedRedux<S extends keyof ProjectStoreState>(219project_id: { project_id: string },220field: S,221): ProjectStoreState[S];222223export function useTypedRedux(224a: keyof StoreStates | { project_id: string },225field: string,226) {227if (typeof a == "string") {228return useRedux(a, field);229}230return useRedux(a.project_id, field);231}232233export function useEditorRedux<State>(editor: {234project_id: string;235path: string;236}) {237function f<S extends keyof State>(field: S): State[S] {238return useReduxEditorStore(239[field as string],240editor.project_id,241editor.path,242) as any;243}244return f;245}246247/*248export function useEditorRedux<State, S extends keyof State>(editor: {249project_id: string;250path: string;251}): State[S] {252return useReduxEditorStore(253[S as string],254editor.project_id,255editor.path256) as any;257}258*/259/*260export function useEditorRedux(261editor: { project_id: string; path: string },262field263): any {264return useReduxEditorStore(265[field as string],266editor.project_id,267editor.path268) as any;269}270*/271272export function useRedux(273path: string | string[],274project_id?: string,275filename?: string,276) {277if (typeof path == "string") {278// good typed version!! -- path specifies store279if (typeof project_id != "string" || typeof filename != "undefined") {280throw Error(281"if first argument of useRedux is a string then second argument must also be and no other arguments can be specified",282);283}284if (is_valid_uuid_string(path)) {285return useRedux([project_id], path);286} else {287return useRedux([path, project_id]);288}289}290if (project_id == null) {291return useReduxNamedStore(path);292}293if (filename == null) {294if (!is_valid_uuid_string(project_id)) {295// this is used a lot by frame-tree editors right now.296return useReduxNamedStore([project_id].concat(path));297} else {298return useReduxProjectStore(path, project_id);299}300}301return useReduxEditorStore(path, project_id, filename);302}303304/*305Hook to get the actions associated to a named actions/store,306a project, or an editor. If the first argument is a uuid,307then it's the project actions or editor actions; otherwise,308it's one of the other named actions or undefined.309*/310311// TODO: very incomplete -- might not even work.312/*313export interface ActionsTypes {314account: types.AccountActions;315"admin-site-licenses": types.SiteLicensesActions;316"admin-users": types.AdminUsersActions;317billing: types.BillingActions;318compute_images: types.ComputeImagesActions;319customize: types.CustomizeActions;320file_use: types.FileUseActions;321mentions: types.MentionsActions;322page: types.PageActions;323projects: types.ProjectsActions;324users: types.UsersActions;325}326327*/328329export function useActions(name: "account"): types.AccountActions;330export function useActions(331name: "admin-site-licenses",332): types.SiteLicensesActions;333export function useActions(name: "admin-users"): types.AdminUsersActions;334export function useActions(name: "billing"): types.BillingActions;335export function useActions(name: "file_use"): types.FileUseActions;336export function useActions(name: "mentions"): types.MentionsActions;337export function useActions(name: "page"): types.PageActions;338export function useActions(name: "projects"): types.ProjectsActions;339export function useActions(name: "users"): types.UsersActions;340export function useActions(name: "news"): types.NewsActions;341export function useActions(name: "customize"): types.CustomizeActions;342343// If it is none of the explicitly named ones... it's a project or just some general actions.344// That said *always* use {project_id} as below to get the actions for a project, so you345// get proper typing.346export function useActions(x: string): any;347348export function useActions<T>(x: { name: string }): T;349350// Return type includes undefined because the actions for a project *do* get351// destroyed when closing a project, and rendering can still happen during this352// time, so client code must account for this.353export function useActions(x: {354project_id: string;355}): ProjectActions | undefined;356357// Or an editor actions (any for now)358export function useActions(x: string, path: string): any;359360export function useActions(x, path?: string) {361return React.useMemo(() => {362let actions;363if (path != null) {364actions = redux.getEditorActions(x, path);365} else {366if (x?.name != null) {367actions = redux.getActions(x.name);368} else if (x?.project_id != null) {369// return here to avoid null check below; it can be null370return redux.getProjectActions(x.project_id);371} else if (is_valid_uuid_string(x)) {372// return here to avoid null check below; it can be null373return redux.getProjectActions(x);374} else {375actions = redux.getActions(x);376}377}378if (actions == null) {379throw Error(`BUG: actions for "${path}" must be defined but is not`);380}381return actions;382}, [x, path]);383}384385// WARNING: I tried to define this Stores interface386// in actions-and-stores.ts but it did NOT work. All387// the types just became any or didn't match. Don't388// move this unless you also fully test it!!389import { Store } from "@cocalc/util/redux/Store";390import { isEqual } from "lodash";391export interface Stores {392account: types.AccountStore;393"admin-site-licenses": types.SiteLicensesStore;394"admin-users": types.AdminUsersStore;395billing: types.BillingStore;396compute_images: types.ComputeImagesStore;397customize: types.CustomizeStore;398file_use: types.FileUseStore;399mentions: types.MentionsStore;400page: types.PageStore;401projects: types.ProjectsStore;402users: types.UsersStore;403news: types.NewsStore;404}405406// If it is none of the explicitly named ones... it's a project.407//export function useStore(name: "projects"): types.ProjectsStore;408export function useStore<T extends keyof Stores>(name: T): Stores[T];409export function useStore(x: { project_id: string }): ProjectStore;410export function useStore<T>(x: { name: string }): T;411// Or an editor store (any for now):412//export function useStore(project_id: string, path: string): Store<any>;413export function useStore(x): any {414return React.useMemo(() => {415let store;416if (x?.project_id != null) {417store = redux.getProjectStore(x.project_id);418} else if (x?.name != null) {419store = redux.getStore(x.name);420} else if (is_valid_uuid_string(x)) {421store = redux.getProjectStore(x);422} else {423store = redux.getStore(x);424}425if (store == null) {426throw Error("store must be defined");427}428return store;429}, [x]) as Store<any>;430}431432// Debug which props changed in a component433export function useTraceUpdate(props) {434const prev = useRef(props);435useEffect(() => {436const changedProps = Object.entries(props).reduce((ps, [k, v]) => {437if (!isEqual(prev.current[k], v)) {438ps[k] = [prev.current[k], v];439}440return ps;441}, {});442if (Object.keys(changedProps).length > 0) {443console.log("Changed props:", changedProps);444}445prev.current = props;446});447}448449450