Przeglądaj źródła

Merge pull request #6236 from overleaf/ta-project-context-strict

Strict Project Context

GitOrigin-RevId: a0f7f2b3dcb29fbd0102dcb920cf5424a921d583
Timothée Alby 4 lat temu
rodzic
commit
6319455e91

+ 2 - 2
services/web/frontend/js/features/file-tree/contexts/file-tree-selectable.js

@@ -75,7 +75,7 @@ function fileTreeSelectableReadOnlyReducer(selectedEntityIds, action) {
 }
 
 export function FileTreeSelectableProvider({ onSelect, children }) {
-  const { _id: projectId, rootDoc_id: rootDocId } = useProjectContext(
+  const { _id: projectId, rootDocId } = useProjectContext(
     projectContextPropTypes
   )
   const { permissionsLevel } = useEditorContext(editorContextPropTypes)
@@ -187,7 +187,7 @@ FileTreeSelectableProvider.propTypes = {
 
 const projectContextPropTypes = {
   _id: PropTypes.string.isRequired,
-  rootDoc_id: PropTypes.string,
+  rootDocId: PropTypes.string,
 }
 
 const editorContextPropTypes = {

+ 8 - 6
services/web/frontend/js/features/pdf-preview/util/compiler.js

@@ -15,7 +15,8 @@ const searchParams = new URLSearchParams(window.location.search)
 
 export default class DocumentCompiler {
   constructor({
-    project,
+    projectId,
+    rootDocId,
     setChangedAt,
     setCompiling,
     setData,
@@ -24,7 +25,8 @@ export default class DocumentCompiler {
     cleanupCompileResult,
     signal,
   }) {
-    this.project = project
+    this.projectId = projectId
+    this.rootDocId = rootDocId
     this.setChangedAt = setChangedAt
     this.setCompiling = setCompiling
     this.setData = setData
@@ -84,7 +86,7 @@ export default class DocumentCompiler {
       const t0 = performance.now()
 
       const data = await postJSON(
-        `/project/${this.project._id}/compile?${params}`,
+        `/project/${this.projectId}/compile?${params}`,
         {
           body: {
             rootDoc_id: this.getRootDocOverrideId(),
@@ -122,7 +124,7 @@ export default class DocumentCompiler {
   // if it contains "\documentclass" then use this as the root doc
   getRootDocOverrideId() {
     // only override when not in the root doc itself
-    if (this.currentDoc.doc_id !== this.project.rootDoc_id) {
+    if (this.currentDoc.doc_id !== this.rootDocId) {
       const snapshot = this.currentDoc.getSnapshot()
 
       if (snapshot && isMainFile(snapshot)) {
@@ -177,7 +179,7 @@ export default class DocumentCompiler {
 
     const params = this.buildPostCompileParams()
 
-    return postJSON(`/project/${this.project._id}/compile/stop?${params}`, {
+    return postJSON(`/project/${this.projectId}/compile/stop?${params}`, {
       signal: this.signal,
     })
       .catch(error => {
@@ -193,7 +195,7 @@ export default class DocumentCompiler {
   clearCache() {
     const params = this.buildPostCompileParams()
 
-    return deleteJSON(`/project/${this.project._id}/output?${params}`, {
+    return deleteJSON(`/project/${this.projectId}/output?${params}`, {
       signal: this.signal,
     }).catch(error => {
       console.error(error)

+ 9 - 9
services/web/frontend/js/features/share-project-modal/components/add-collaborators.js

@@ -20,11 +20,11 @@ export default function AddCollaborators() {
 
   const { updateProject, setInFlight, setError } = useShareProjectContext()
 
-  const project = useProjectContext()
+  const { _id: projectId, members, invites } = useProjectContext()
 
   const currentMemberEmails = useMemo(
-    () => (project.members || []).map(member => member.email).sort(),
-    [project.members]
+    () => (members || []).map(member => member.email).sort(),
+    [members]
   )
 
   const nonMemberContacts = useMemo(() => {
@@ -73,14 +73,14 @@ export default function AddCollaborators() {
       let data
 
       try {
-        const invite = (project.invites || []).find(
+        const invite = (invites || []).find(
           invite => invite.email === normalisedEmail
         )
 
         if (invite) {
-          data = await resendInvite(project, invite)
+          data = await resendInvite(projectId, invite)
         } else {
-          data = await sendInvite(project, email, privileges)
+          data = await sendInvite(projectId, email, privileges)
         }
       } catch (error) {
         setInFlight(false)
@@ -98,15 +98,15 @@ export default function AddCollaborators() {
         setInFlight(false)
       } else if (data.invite) {
         updateProject({
-          invites: project.invites.concat(data.invite),
+          invites: invites.concat(data.invite),
         })
       } else if (data.users) {
         updateProject({
-          members: project.members.concat(data.users),
+          members: members.concat(data.users),
         })
       } else if (data.user) {
         updateProject({
-          members: project.members.concat(data.user),
+          members: members.concat(data.user),
         })
       }
 

+ 11 - 9
services/web/frontend/js/features/share-project-modal/components/edit-member.js

@@ -27,7 +27,7 @@ export default function EditMember({ member }) {
   }, [member.privileges])
 
   const { updateProject, monitorRequest } = useShareProjectContext()
-  const project = useProjectContext()
+  const { _id: projectId, members } = useProjectContext()
 
   function handleSubmit(event) {
     event.preventDefault()
@@ -36,12 +36,12 @@ export default function EditMember({ member }) {
       setConfirmingOwnershipTransfer(true)
     } else {
       monitorRequest(() =>
-        updateMember(project, member, {
+        updateMember(projectId, member, {
           privilegeLevel: privileges,
         })
       ).then(() => {
         updateProject({
-          members: project.members.map(item =>
+          members: members.map(item =>
             item._id === member._id ? { ...item, privileges } : item
           ),
         })
@@ -118,16 +118,18 @@ SelectPrivilege.propTypes = {
 function RemoveMemberAction({ member }) {
   const { t } = useTranslation()
   const { updateProject, monitorRequest } = useShareProjectContext()
-  const project = useProjectContext()
+  const { _id: projectId, members } = useProjectContext()
 
   function handleClick(event) {
     event.preventDefault()
 
-    monitorRequest(() => removeMemberFromProject(project, member)).then(() => {
-      updateProject({
-        members: project.members.filter(existing => existing !== member),
-      })
-    })
+    monitorRequest(() => removeMemberFromProject(projectId, member)).then(
+      () => {
+        updateProject({
+          members: members.filter(existing => existing !== member),
+        })
+      }
+    )
   }
 
   return (

+ 6 - 6
services/web/frontend/js/features/share-project-modal/components/invite.js

@@ -41,20 +41,20 @@ Invite.propTypes = {
 
 function ResendInvite({ invite }) {
   const { monitorRequest } = useShareProjectContext()
-  const project = useProjectContext()
+  const { _id: projectId } = useProjectContext()
 
   // const buttonRef = useRef(null)
   //
   const handleClick = useCallback(
     () =>
-      monitorRequest(() => resendInvite(project, invite)).finally(() => {
+      monitorRequest(() => resendInvite(projectId, invite)).finally(() => {
         // NOTE: disabled as react-bootstrap v0.33.1 isn't forwarding the ref to the `button`
         // if (buttonRef.current) {
         //   buttonRef.current.blur()
         // }
         document.activeElement.blur()
       }),
-    [invite, monitorRequest, project]
+    [invite, monitorRequest, projectId]
   )
 
   return (
@@ -75,14 +75,14 @@ ResendInvite.propTypes = {
 function RevokeInvite({ invite }) {
   const { t } = useTranslation()
   const { updateProject, monitorRequest } = useShareProjectContext()
-  const project = useProjectContext()
+  const { _id: projectId, invites } = useProjectContext()
 
   function handleClick(event) {
     event.preventDefault()
 
-    monitorRequest(() => revokeInvite(project, invite)).then(() => {
+    monitorRequest(() => revokeInvite(projectId, invite)).then(() => {
       updateProject({
-        invites: project.invites.filter(existing => existing !== invite),
+        invites: invites.filter(existing => existing !== invite),
       })
     })
   }

+ 14 - 12
services/web/frontend/js/features/share-project-modal/components/link-sharing.js

@@ -14,27 +14,29 @@ export default function LinkSharing() {
 
   const { monitorRequest } = useShareProjectContext()
 
-  const project = useProjectContext()
+  const { _id: projectId, publicAccessLevel } = useProjectContext()
 
   // set the access level of a project
   const setAccessLevel = useCallback(
-    publicAccesLevel => {
+    newPublicAccessLevel => {
       setInflight(true)
-      monitorRequest(() => setProjectAccessLevel(project, publicAccesLevel))
+      monitorRequest(() =>
+        setProjectAccessLevel(projectId, newPublicAccessLevel)
+      )
         .then(() => {
           // NOTE: not calling `updateProject` here as it receives data via
           // project:publicAccessLevel:changed and project:tokens:changed
           // over the websocket connection
-          // TODO: eventTracking.sendMB('project-make-token-based') when publicAccesLevel is 'tokenBased'
+          // TODO: eventTracking.sendMB('project-make-token-based') when newPublicAccessLevel is 'tokenBased'
         })
         .finally(() => {
           setInflight(false)
         })
     },
-    [monitorRequest, project]
+    [monitorRequest, projectId]
   )
 
-  switch (project.publicAccesLevel) {
+  switch (publicAccessLevel) {
     // Private (with token-access available)
     case 'private':
       return (
@@ -56,7 +58,7 @@ export default function LinkSharing() {
       return (
         <LegacySharing
           setAccessLevel={setAccessLevel}
-          accessLevel={project.publicAccesLevel}
+          accessLevel={publicAccessLevel}
           inflight={inflight}
         />
       )
@@ -96,7 +98,7 @@ PrivateSharing.propTypes = {
 }
 
 function TokenBasedSharing({ setAccessLevel, inflight }) {
-  const project = useProjectContext()
+  const { tokens } = useProjectContext()
 
   return (
     <Row className="public-access-level">
@@ -122,7 +124,7 @@ function TokenBasedSharing({ setAccessLevel, inflight }) {
             <Trans i18nKey="anyone_with_link_can_edit" />
           </strong>
           <AccessToken
-            token={project?.tokens?.readAndWrite}
+            token={tokens?.readAndWrite}
             path="/"
             tooltipId="tooltip-copy-link-rw"
           />
@@ -132,7 +134,7 @@ function TokenBasedSharing({ setAccessLevel, inflight }) {
             <Trans i18nKey="anyone_with_link_can_view" />
           </strong>
           <AccessToken
-            token={project?.tokens?.readOnly}
+            token={tokens?.readOnly}
             path="/read/"
             tooltipId="tooltip-copy-link-ro"
           />
@@ -181,7 +183,7 @@ LegacySharing.propTypes = {
 }
 
 export function ReadOnlyTokenLink() {
-  const project = useProjectContext()
+  const { tokens } = useProjectContext()
 
   return (
     <Row className="public-access-level">
@@ -191,7 +193,7 @@ export function ReadOnlyTokenLink() {
             <Trans i18nKey="anyone_with_link_can_view" />
           </strong>
           <AccessToken
-            token={project?.tokens?.readOnly}
+            token={tokens?.readOnly}
             path="/read/"
             tooltipId="tooltip-copy-link-ro"
           />

+ 2 - 2
services/web/frontend/js/features/share-project-modal/components/owner-info.js

@@ -3,11 +3,11 @@ import { Col, Row } from 'react-bootstrap'
 import { Trans } from 'react-i18next'
 
 export default function OwnerInfo() {
-  const project = useProjectContext()
+  const { owner } = useProjectContext()
 
   return (
     <Row className="project-member">
-      <Col xs={7}>{project.owner?.email}</Col>
+      <Col xs={7}>{owner?.email}</Col>
       <Col xs={3} className="text-left">
         <Trans i18nKey="owner" />
       </Col>

+ 2 - 2
services/web/frontend/js/features/share-project-modal/components/send-invites-notice.js

@@ -4,12 +4,12 @@ import { Trans } from 'react-i18next'
 import { useProjectContext } from '../../../shared/context/project-context'
 
 export default function SendInvitesNotice() {
-  const project = useProjectContext()
+  const { publicAccessLevel } = useProjectContext()
 
   return (
     <Row className="public-access-level public-access-level--notice">
       <Col xs={12} className="text-center">
-        <AccessLevel level={project.publicAccesLevel} />
+        <AccessLevel level={publicAccessLevel} />
       </Col>
     </Row>
   )

+ 5 - 8
services/web/frontend/js/features/share-project-modal/components/send-invites.js

@@ -5,24 +5,21 @@ import AddCollaboratorsUpgrade from './add-collaborators-upgrade'
 import { useProjectContext } from '../../../shared/context/project-context'
 
 export default function SendInvites() {
-  const project = useProjectContext()
+  const { members, invites, features } = useProjectContext()
 
   // whether the project has not reached the collaborator limit
   const canAddCollaborators = useMemo(() => {
-    if (!project) {
+    if (!features) {
       return false
     }
 
-    if (project.features.collaborators === -1) {
+    if (features.collaborators === -1) {
       // infinite collaborators
       return true
     }
 
-    return (
-      project.members.length + project.invites.length <
-      project.features.collaborators
-    )
-  }, [project])
+    return members.length + invites.length < features.collaborators
+  }, [members, invites, features])
 
   return (
     <Row className="invite-controls">

+ 7 - 7
services/web/frontend/js/features/share-project-modal/components/share-modal-body.js

@@ -18,7 +18,7 @@ export default function ShareModalBody() {
     splitTestVariants: PropTypes.object,
   })
 
-  const project = useProjectContext()
+  const { invites, members } = useProjectContext()
 
   switch (splitTestVariants['project-share-modal-paywall']) {
     case 'new-copy-top':
@@ -35,7 +35,7 @@ export default function ShareModalBody() {
 
           <OwnerInfo />
 
-          {project.members.map(member =>
+          {members.map(member =>
             isAdmin ? (
               <EditMember key={member._id} member={member} />
             ) : (
@@ -43,7 +43,7 @@ export default function ShareModalBody() {
             )
           )}
 
-          {project.invites.map(invite => (
+          {invites.map(invite => (
             <Invite key={invite._id} invite={invite} isAdmin={isAdmin} />
           ))}
 
@@ -64,7 +64,7 @@ export default function ShareModalBody() {
         <>
           <OwnerInfo />
 
-          {project.members.map(member =>
+          {members.map(member =>
             isAdmin ? (
               <EditMember key={member._id} member={member} />
             ) : (
@@ -72,7 +72,7 @@ export default function ShareModalBody() {
             )
           )}
 
-          {project.invites.map(invite => (
+          {invites.map(invite => (
             <Invite key={invite._id} invite={invite} isAdmin={isAdmin} />
           ))}
 
@@ -99,7 +99,7 @@ export default function ShareModalBody() {
 
           <OwnerInfo />
 
-          {project.members.map(member =>
+          {members.map(member =>
             isAdmin ? (
               <EditMember key={member._id} member={member} />
             ) : (
@@ -107,7 +107,7 @@ export default function ShareModalBody() {
             )
           )}
 
-          {project.invites.map(invite => (
+          {invites.map(invite => (
             <Invite key={invite._id} invite={invite} isAdmin={isAdmin} />
           ))}
 

+ 4 - 27
services/web/frontend/js/features/share-project-modal/components/share-project-modal.js

@@ -7,7 +7,10 @@ import React, {
 } from 'react'
 import PropTypes from 'prop-types'
 import ShareProjectModalContent from './share-project-modal-content'
-import { useProjectContext } from '../../../shared/context/project-context'
+import {
+  useProjectContext,
+  projectShape,
+} from '../../../shared/context/project-context'
 import { useSplitTestContext } from '../../../shared/context/split-test-context'
 import { sendMB } from '../../../infrastructure/event-tracking'
 
@@ -37,32 +40,6 @@ export function useShareProjectContext() {
   return context
 }
 
-const projectShape = {
-  _id: PropTypes.string.isRequired,
-  members: PropTypes.arrayOf(
-    PropTypes.shape({
-      _id: PropTypes.string.isRequired,
-    })
-  ),
-  invites: PropTypes.arrayOf(
-    PropTypes.shape({
-      _id: PropTypes.string.isRequired,
-    })
-  ),
-  name: PropTypes.string,
-  features: PropTypes.shape({
-    collaborators: PropTypes.number,
-  }),
-  publicAccesLevel: PropTypes.string,
-  tokens: PropTypes.shape({
-    readOnly: PropTypes.string,
-    readAndWrite: PropTypes.string,
-  }),
-  owner: PropTypes.shape({
-    email: PropTypes.string,
-  }),
-}
-
 const ShareProjectModal = React.memo(function ShareProjectModal({
   handleHide,
   show,

+ 3 - 3
services/web/frontend/js/features/share-project-modal/components/transfer-ownership-modal.js

@@ -12,13 +12,13 @@ export default function TransferOwnershipModal({ member, cancel }) {
   const [inflight, setInflight] = useState(false)
   const [error, setError] = useState(false)
 
-  const project = useProjectContext()
+  const { _id: projectId, name: projectName } = useProjectContext()
 
   function confirm() {
     setError(false)
     setInflight(true)
 
-    transferProjectOwnership(project, member)
+    transferProjectOwnership(projectId, member)
       .then(() => {
         reload()
       })
@@ -39,7 +39,7 @@ export default function TransferOwnershipModal({ member, cancel }) {
         <p>
           <Trans
             i18nKey="project_ownership_transfer_confirmation_1"
-            values={{ user: member.email, project: project.name }}
+            values={{ user: member.email, project: projectName }}
             components={[<strong key="strong-1" />, <strong key="strong-2" />]}
           />
         </p>

+ 2 - 2
services/web/frontend/js/features/share-project-modal/controllers/react-share-project-modal-controller.js

@@ -45,7 +45,7 @@ export default App.controller(
 
     ide.socket.on('project:membership:changed', data => {
       if (data.members) {
-        listProjectMembers($scope.project)
+        listProjectMembers($scope.project._id)
           .then(({ members }) => {
             if (members) {
               $scope.$applyAsync(() => {
@@ -59,7 +59,7 @@ export default App.controller(
       }
 
       if (data.invites) {
-        listProjectInvites($scope.project)
+        listProjectInvites($scope.project._id)
           .then(({ invites }) => {
             if (invites) {
               $scope.$applyAsync(() => {

+ 18 - 24
services/web/frontend/js/features/share-project-modal/utils/api.js

@@ -6,11 +6,11 @@ import {
 } from '../../../infrastructure/fetch-json'
 import { executeV2Captcha } from './captcha'
 
-export function sendInvite(project, email, privileges) {
+export function sendInvite(projectId, email, privileges) {
   return executeV2Captcha(
     window.ExposedSettings.recaptchaDisabled?.invite
   ).then(grecaptchaResponse => {
-    return postJSON(`/project/${project._id}/invite`, {
+    return postJSON(`/project/${projectId}/invite`, {
       body: {
         email, // TODO: normalisedEmail?
         privileges,
@@ -20,48 +20,42 @@ export function sendInvite(project, email, privileges) {
   })
 }
 
-export function resendInvite(project, invite) {
-  return postJSON(`/project/${project._id}/invite/${invite._id}/resend`)
+export function resendInvite(projectId, invite) {
+  return postJSON(`/project/${projectId}/invite/${invite._id}/resend`)
 }
 
-export function revokeInvite(project, invite) {
-  return deleteJSON(`/project/${project._id}/invite/${invite._id}`)
+export function revokeInvite(projectId, invite) {
+  return deleteJSON(`/project/${projectId}/invite/${invite._id}`)
 }
 
-export function updateMember(project, member, data) {
-  return putJSON(`/project/${project._id}/users/${member._id}`, {
+export function updateMember(projectId, member, data) {
+  return putJSON(`/project/${projectId}/users/${member._id}`, {
     body: data,
   })
 }
 
-export function removeMemberFromProject(project, member) {
-  return deleteJSON(`/project/${project._id}/users/${member._id}`)
+export function removeMemberFromProject(projectId, member) {
+  return deleteJSON(`/project/${projectId}/users/${member._id}`)
 }
 
-export function transferProjectOwnership(project, member) {
-  return postJSON(`/project/${project._id}/transfer-ownership`, {
+export function transferProjectOwnership(projectId, member) {
+  return postJSON(`/project/${projectId}/transfer-ownership`, {
     body: {
       user_id: member._id,
     },
   })
 }
 
-export function setProjectAccessLevel(project, publicAccessLevel) {
-  return postJSON(`/project/${project._id}/settings/admin`, {
+export function setProjectAccessLevel(projectId, publicAccessLevel) {
+  return postJSON(`/project/${projectId}/settings/admin`, {
     body: { publicAccessLevel },
   })
 }
 
-// export function updateProjectAdminSettings(project, data) {
-//   return postJSON(`/project/${project._id}/settings/admin`, {
-//     body: data
-//   })
-// }
-
-export function listProjectMembers(project) {
-  return getJSON(`/project/${project._id}/members`)
+export function listProjectMembers(projectId) {
+  return getJSON(`/project/${projectId}/members`)
 }
 
-export function listProjectInvites(project) {
-  return getJSON(`/project/${project._id}/invites`)
+export function listProjectInvites(projectId) {
+  return getJSON(`/project/${projectId}/invites`)
 }

+ 3 - 4
services/web/frontend/js/shared/context/compile-context.js

@@ -64,9 +64,7 @@ export function CompileProvider({ children }) {
 
   const { hasPremiumCompile, isProjectOwner } = useEditorContext()
 
-  const project = useProjectContext()
-
-  const projectId = project._id
+  const { _id: projectId, rootDocId } = useProjectContext()
 
   // whether a compile is in progress
   const [compiling, setCompiling] = useState(false)
@@ -169,7 +167,8 @@ export function CompileProvider({ children }) {
   // the document compiler
   const [compiler] = useState(() => {
     return new DocumentCompiler({
-      project,
+      projectId,
+      rootDocId,
       setChangedAt,
       setCompiling,
       setData,

+ 66 - 34
services/web/frontend/js/shared/context/project-context.js

@@ -4,41 +4,43 @@ import useScopeValue from '../hooks/use-scope-value'
 
 const ProjectContext = createContext()
 
-ProjectContext.Provider.propTypes = {
-  value: PropTypes.shape({
-    _id: PropTypes.string.isRequired,
-    name: PropTypes.string.isRequired,
-    rootDoc_id: PropTypes.string,
-    members: PropTypes.arrayOf(
-      PropTypes.shape({
-        _id: PropTypes.string.isRequired,
-      })
-    ),
-    invites: PropTypes.arrayOf(
-      PropTypes.shape({
-        _id: PropTypes.string.isRequired,
-      })
-    ),
-    features: PropTypes.shape({
-      collaborators: PropTypes.number,
-      compileGroup: PropTypes.oneOf(['alpha', 'standard', 'priority']),
-      trackChangesVisible: PropTypes.bool,
-      references: PropTypes.bool,
-      mendeley: PropTypes.bool,
-      zotero: PropTypes.bool,
-    }),
-    publicAccesLevel: PropTypes.string,
-    tokens: PropTypes.shape({
-      readOnly: PropTypes.string,
-      readAndWrite: PropTypes.string,
-    }),
-    owner: PropTypes.shape({
+export const projectShape = {
+  _id: PropTypes.string.isRequired,
+  name: PropTypes.string.isRequired,
+  rootDocId: PropTypes.string,
+  members: PropTypes.arrayOf(
+    PropTypes.shape({
+      _id: PropTypes.string.isRequired,
+    })
+  ),
+  invites: PropTypes.arrayOf(
+    PropTypes.shape({
       _id: PropTypes.string.isRequired,
-      email: PropTypes.string.isRequired,
-    }),
+    })
+  ),
+  features: PropTypes.shape({
+    collaborators: PropTypes.number,
+    compileGroup: PropTypes.oneOf(['alpha', 'standard', 'priority']),
+    trackChangesVisible: PropTypes.bool,
+    references: PropTypes.bool,
+    mendeley: PropTypes.bool,
+    zotero: PropTypes.bool,
+  }),
+  publicAccessLevel: PropTypes.string,
+  tokens: PropTypes.shape({
+    readOnly: PropTypes.string,
+    readAndWrite: PropTypes.string,
+  }),
+  owner: PropTypes.shape({
+    _id: PropTypes.string.isRequired,
+    email: PropTypes.string.isRequired,
   }),
 }
 
+ProjectContext.Provider.propTypes = {
+  value: PropTypes.shape(projectShape),
+}
+
 export function useProjectContext(propTypes) {
   const context = useContext(ProjectContext)
 
@@ -70,12 +72,42 @@ const projectFallback = {
 export function ProjectProvider({ children }) {
   const [project] = useScopeValue('project', true)
 
+  const {
+    _id,
+    name,
+    rootDoc_id: rootDocId,
+    members,
+    invites,
+    features,
+    publicAccesLevel: publicAccessLevel,
+    tokens,
+    owner,
+  } = project || projectFallback
+
   const value = useMemo(() => {
     return {
-      ...projectFallback,
-      ...project,
+      _id,
+      name,
+      rootDocId,
+      members,
+      invites,
+      features,
+      publicAccessLevel,
+      tokens,
+      owner,
     }
-  }, [project])
+  }, [
+    _id,
+    name,
+    rootDocId,
+    members,
+    invites,
+    features,
+    publicAccessLevel,
+    tokens,
+    owner,
+  ])
+
   return (
     <ProjectContext.Provider value={value}>{children}</ProjectContext.Provider>
   )