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

[WIP] Avoid forcing useless renders #1569

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
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
4 changes: 2 additions & 2 deletions docs/api/cozy-client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -956,7 +956,7 @@ Retrieve intance info like context, uuid, disk usage etc

*Defined in*

[packages/cozy-client/src/hooks/useQuery.js:93](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/hooks/useQuery.js#L93)
[packages/cozy-client/src/hooks/useQuery.js:94](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/hooks/useQuery.js#L94)

***

Expand All @@ -979,7 +979,7 @@ Fetches a queryDefinition and returns the queryState

*Defined in*

[packages/cozy-client/src/hooks/useQuery.js:28](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/hooks/useQuery.js#L28)
[packages/cozy-client/src/hooks/useQuery.js:29](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/hooks/useQuery.js#L29)

***

Expand Down
187 changes: 94 additions & 93 deletions docs/api/cozy-client/classes/CozyClient.md

Large diffs are not rendered by default.

31 changes: 25 additions & 6 deletions packages/cozy-client/src/CozyClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@
* @property {import("./types").AppMetadata} [appMetadata] - Metadata about the application that will be used in ensureCozyMetadata
* @property {import("./types").ClientCapabilities} [capabilities] - Capabilities sent by the stack
* @property {boolean} [store] - If set to false, the client will not instantiate a Redux store automatically. Use this if you want to merge cozy-client's store with your own redux store. See [here](https://docs.cozy.io/en/cozy-client/react-integration/#1b-use-your-own-redux-store) for more information.
* @property {boolean} [forceHydratation] - If set to true, all documents will be hydrated w.r.t. the provided schema's relationships, even if the relationship does not exist on the doc.
*/

/**
Expand Down Expand Up @@ -1105,7 +1106,7 @@
* Save the document or array of documents into the persisted storage (if any)
*
* @private
* @param {CozyClientDocument | Array<CozyClientDocument>} data - Document or array of documents to be saved

Check warning on line 1109 in packages/cozy-client/src/CozyClient.js

View workflow job for this annotation

GitHub Actions / Build and publish

Expected @param names to be "definition, data". Got "data"
* @returns {Promise<void>}
*/
async persistVirtualDocuments(definition, data) {
Expand Down Expand Up @@ -1193,7 +1194,7 @@
if (queryDef instanceof QueryDefinition) {
definitions.push(queryDef)
} else {
documents.push(queryDef)
documents.push(doc)
Copy link
Member

Choose a reason for hiding this comment

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

Is this part of the fix? I don't understand the impact compared to the previous implementation

}
} catch {
// eslint-disable-next-line
Expand Down Expand Up @@ -1309,9 +1310,11 @@

hydrateRelationships(document, schemaRelationships) {
const methods = this.getRelationshipStoreAccessors()
return mapValues(schemaRelationships, (assoc, name) =>
createAssociation(document, assoc, methods)
)
return mapValues(schemaRelationships, (assoc, name) => {
if (this.options?.forceHydratation || document.relationships?.[assoc]) {
return createAssociation(document, assoc, methods)
}
})
}

/**
Expand Down Expand Up @@ -1421,13 +1424,29 @@
return queryResults
}

const data =
const hydratedData =
hydrated && doctype
? this.hydrateDocuments(doctype, queryResults.data)
: queryResults.data

const relationships = this.schema.getDoctypeSchema(doctype)?.relationships
const relationshipNames = relationships
? Object.keys(relationships)
: null

// The `data` array contains the hydrated data with the relationships, if any.
// The `storeData` array contains the documents from the store: this is useful to preserve
// referential equality, to be later evaluated to determine whether or not the
// documents had changed.
return {
...queryResults,
data: isSingleDocQuery && singleDocData ? data[0] : data
data:
isSingleDocQuery && singleDocData ? hydratedData[0] : hydratedData,
storeData:
isSingleDocQuery && singleDocData
? queryResults.data[0]
: queryResults.data,
relationshipNames
}
} catch (e) {
logger.warn(
Expand Down Expand Up @@ -1852,8 +1871,8 @@
* @template {string} T
*
* @param {string} slug - the cozy-app's slug containing the setting (can be 'instance' for global settings)
* @param {T[]} keys - The names of the settings to retrieve

Check warning on line 1874 in packages/cozy-client/src/CozyClient.js

View workflow job for this annotation

GitHub Actions / Build and publish

The type 'T' is undefined
* @returns {Promise<Record<T, any>>} - The value of the requested setting

Check warning on line 1875 in packages/cozy-client/src/CozyClient.js

View workflow job for this annotation

GitHub Actions / Build and publish

The type 'T' is undefined
*/
async getSettings(slug, keys) {
return getSettings(this, slug, keys)
Expand Down
3 changes: 2 additions & 1 deletion packages/cozy-client/src/hooks/useQuery.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import useClient from './useClient'
import logger from '../logger'
import { clientContext } from '../context'
import { QueryDefinition } from '../queries/dsl'
import { equalityCheckForQuery } from './utils'

const useSelector = createSelectorHook(clientContext)

Expand Down Expand Up @@ -61,7 +62,7 @@ const useQuery = (queryDefinition, options) => {
hydrated: get(options, 'hydrated', true),
singleDocData: get(options, 'singleDocData', false)
})
})
}, equalityCheckForQuery)

useEffect(
() => {
Expand Down
107 changes: 107 additions & 0 deletions packages/cozy-client/src/hooks/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/**
* Equality check
*
* Note we do not make a shallow equality check on documents, as it is less efficient and should
* not be necessary: the queryResult.data is built by extracting documents from the state, thus
* preserving references.
*
* @param {import("../types").QueryStateResult} queryResA - A query result to compare
* @param {import("../types").QueryStateResult} queryResB - A query result to compare
* @returns
*/
export const equalityCheckForQuery = (queryResA, queryResB) => {
//console.log('Call equality check : ', queryResA, queryResB)
if (queryResA === queryResB) {
// Referential equality
return true
}

if (
typeof queryResA !== 'object' ||
queryResA === null ||
typeof queryResB !== 'object' ||
queryResB === null
) {
// queryResA or queryResB is not an object or null
return false
}

if (queryResA.id !== queryResB.id) {
return false
}
if (queryResA.fetchStatus !== queryResB.fetchStatus) {
return false
}

const docsA = queryResA.storeData
const docsB = queryResB.storeData
if (!docsA || !docsB) {
// No data to check
return false
}
if (!Array.isArray(docsA) && !Array.isArray(docsB) && docsA !== docsB) {
// Only one doc
return false
}

if (
Array.isArray(docsA) &&
Array.isArray(docsB) &&
!arraysHaveSameLength(docsA, docsB)
) {
// A document was added or removed
return false
}

if (Array.isArray(docsA) && Array.isArray(docsB)) {
for (let i = 0; i < docsA.length; i++) {
if (docsA[i] !== docsB[i]) {
// References should be the same for non-updated documents
return false
}
}
}

if (queryResA.relationshipNames) {
// In case of relationships, we cannot check referential equality, because we
// "hydrate" the data by creating a new instance of the related relationship class.
// Thus, we check the document revision instead.
const hydratedDataA = queryResA.data
const hydratedDataB = queryResB.data
if (!Array.isArray(hydratedDataA) && !Array.isArray(hydratedDataB)) {
// One doc with changed relationship
return revsAreEqual(hydratedDataA, hydratedDataB)
}
if (!arraysHaveSameLength(hydratedDataA, hydratedDataB)) {
// A relationship have been added or removed
return false
}
if (Array.isArray(hydratedDataA) && Array.isArray(hydratedDataB)) {
for (let i = 0; i < hydratedDataA.length; i++) {
for (const name of queryResA.relationshipNames) {
// Check hydrated relationship
const includedA = hydratedDataA[i][name]
const includedB = hydratedDataB[i][name]
if (includedA && includedB) {
if (!revsAreEqual(includedA, includedB)) {
return false
}
}
}
}
}
}
return true
}

const revsAreEqual = (docA, docB) => {
return docA?._rev === docB?._rev
}

const arraysHaveSameLength = (arrayA, arrayB) => {
return (
Array.isArray(arrayA) &&
Array.isArray(arrayB) &&
arrayA.length === arrayB.length
)
}
Loading
Loading