Optimise client library performance with skeleton loaders (#9256)

* Treat empty string default values as nullish when considering whether field default values should be applied to the real field value

* Add flag to allow not loading data immediately upon creation of a data fetch object

* Use loading context inside forms to avoid wasted renders while form schema are loading

* Wait for parent data providers to finish loading before loading their own data

* Optimise data provider to reduce updates as much as possible

* Allow forms to render content immediately again, but use the loading context to inform data providers to wait for them

* Remove fetchOnCreation flag for data fetches as now redundant

* Fix issue with deleting the selected button action when the next action has no parameters

* Lint
This commit is contained in:
Andrew Kingston 2023-01-11 08:01:41 +00:00 committed by GitHub
parent dd58f79763
commit b9cb2d9e78
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 82 additions and 52 deletions

View File

@ -74,8 +74,18 @@
} }
const deleteAction = index => { const deleteAction = index => {
// Check if we're deleting the selected action
const selectedIndex = actions.indexOf(selectedAction)
const isSelected = index === selectedIndex
// Delete the action
actions.splice(index, 1) actions.splice(index, 1)
actions = actions actions = actions
// Select a new action if we deleted the selected one
if (isSelected) {
selectedAction = actions?.length ? actions[0] : null
}
} }
const toggleActionList = () => { const toggleActionList = () => {

View File

@ -171,6 +171,15 @@
$: pad = pad || (interactive && hasChildren && inDndPath) $: pad = pad || (interactive && hasChildren && inDndPath)
$: $dndIsDragging, (pad = false) $: $dndIsDragging, (pad = false)
// Determine whether we should render a skeleton loader for this component
$: showSkeleton =
$loading &&
definition.name !== "Screenslot" &&
children.length === 0 &&
!instance._blockElementHasChildren &&
!definition.block &&
definition.skeleton !== false
// Update component context // Update component context
$: store.set({ $: store.set({
id, id,
@ -473,14 +482,6 @@
componentStore.actions.unregisterInstance(id) componentStore.actions.unregisterInstance(id)
} }
}) })
$: showSkeleton =
$loading &&
definition.name !== "Screenslot" &&
children.length === 0 &&
!instance._blockElementHasChildren &&
!definition.block &&
definition.skeleton !== false
</script> </script>
{#if showSkeleton} {#if showSkeleton}

View File

@ -11,20 +11,23 @@
export let limit export let limit
export let paginate export let paginate
const loading = writable(false)
const { styleable, Provider, ActionTypes, API } = getContext("sdk") const { styleable, Provider, ActionTypes, API } = getContext("sdk")
const component = getContext("component") const component = getContext("component")
// Update loading state
const parentLoading = getContext("loading")
const loading = writable(true)
setContext("loading", loading)
// We need to manage our lucene query manually as we want to allow components // We need to manage our lucene query manually as we want to allow components
// to extend it // to extend it
let queryExtensions = {} let queryExtensions = {}
$: defaultQuery = LuceneUtils.buildLuceneQuery(filter) $: defaultQuery = LuceneUtils.buildLuceneQuery(filter)
$: query = extendQuery(defaultQuery, queryExtensions) $: query = extendQuery(defaultQuery, queryExtensions)
// Keep our data fetch instance up to date // Fetch data and refresh when needed
$: fetch = createFetch(dataSource) $: fetch = createFetch(dataSource, $parentLoading)
$: fetch.update({ $: updateFetch({
query, query,
sortColumn, sortColumn,
sortOrder, sortOrder,
@ -32,6 +35,9 @@
paginate, paginate,
}) })
// Keep loading context updated
$: loading.set($parentLoading || !$fetch.loaded)
// Build our action context // Build our action context
$: actions = [ $: actions = [
{ {
@ -80,14 +86,21 @@
sortColumn: $fetch.sortColumn, sortColumn: $fetch.sortColumn,
sortOrder: $fetch.sortOrder, sortOrder: $fetch.sortOrder,
}, },
limit: limit, limit,
} }
const parentLoading = getContext("loading") const createFetch = (datasource, parentLoading) => {
setContext("loading", loading) // Return a dummy fetch if parent is still loading. We do this so that we
$: loading.set($parentLoading || !$fetch.loaded) // can still properly subscribe to a valid fetch object and check all
// properties, but we want to avoid fetching the real data until all parents
// have finished loading.
// This logic is only needed due to skeleton loaders, as previously we
// simply blocked component rendering until data was ready.
if (parentLoading) {
return fetchData({ API })
}
const createFetch = datasource => { // Otherwise return the real thing
return fetchData({ return fetchData({
API, API,
datasource, datasource,
@ -101,6 +114,14 @@
}) })
} }
const updateFetch = opts => {
// Only update fetch if parents have stopped loading. Otherwise we will
// trigger a fetch of the real data before parents are ready.
if (!$parentLoading) {
fetch.update(opts)
}
}
const addQueryExtension = (key, extension) => { const addQueryExtension = (key, extension) => {
if (!key || !extension) { if (!key || !extension) {
return return

View File

@ -1,7 +1,8 @@
<script> <script>
import { getContext } from "svelte" import { getContext, setContext } from "svelte"
import InnerForm from "./InnerForm.svelte" import InnerForm from "./InnerForm.svelte"
import { Helpers } from "@budibase/bbui" import { Helpers } from "@budibase/bbui"
import { writable } from "svelte/store"
export let dataSource export let dataSource
export let theme export let theme
@ -20,10 +21,17 @@
const context = getContext("context") const context = getContext("context")
const { API, fetchDatasourceSchema } = getContext("sdk") const { API, fetchDatasourceSchema } = getContext("sdk")
// Forms also use loading context as they require loading a schema
const parentLoading = getContext("loading")
const loading = writable(true)
setContext("loading", loading)
let loaded = false
let schema let schema
let table let table
$: fetchSchema(dataSource) $: fetchSchema(dataSource)
$: loading.set($parentLoading || !loaded)
// Returns the closes data context which isn't a built in context // Returns the closes data context which isn't a built in context
const getInitialValues = (type, dataSource, context) => { const getInitialValues = (type, dataSource, context) => {
@ -55,11 +63,14 @@
} }
const res = await fetchDatasourceSchema(dataSource) const res = await fetchDatasourceSchema(dataSource)
schema = res || {} schema = res || {}
if (!loaded) {
loaded = true
}
} }
$: initialValues = getInitialValues(actionType, dataSource, $context) $: initialValues = getInitialValues(actionType, dataSource, $context)
$: resetKey = Helpers.hashString( $: resetKey = Helpers.hashString(
!!schema + loaded +
JSON.stringify(initialValues) + JSON.stringify(initialValues) +
JSON.stringify(dataSource) + JSON.stringify(dataSource) +
disabled disabled

View File

@ -128,21 +128,15 @@
return fields.find(field => get(field).name === name) return fields.find(field => get(field).name === name)
} }
const getDefault = (defaultValue, schema, type) => { // Sanitises a value by ensuring it doesn't contain any invalid data
// Remove any values not present in the field schema const sanitiseValue = (value, schema, type) => {
// Convert any values supplied to string // Check arrays - remove any values not present in the field schema and
if (Array.isArray(defaultValue) && type == "array" && schema) { // convert any values supplied to strings
return defaultValue.reduce((acc, entry) => { if (Array.isArray(value) && type === "array" && schema) {
let processedOption = String(entry) const options = schema?.constraints.inclusion || []
let schemaOptions = schema.constraints.inclusion return value.map(opt => String(opt)).filter(opt => options.includes(opt))
if (schemaOptions.indexOf(processedOption) > -1) {
acc.push(processedOption)
}
return acc
}, [])
} else {
return defaultValue
} }
return value
} }
const formApi = { const formApi = {
@ -160,7 +154,6 @@
// Create validation function based on field schema // Create validation function based on field schema
const schemaConstraints = schema?.[field]?.constraints const schemaConstraints = schema?.[field]?.constraints
const validator = disableValidation const validator = disableValidation
? null ? null
: createValidatorFromConstraints( : createValidatorFromConstraints(
@ -170,10 +163,11 @@
table table
) )
const parsedDefault = getDefault(defaultValue, schema?.[field], type) // Sanitise the default value to ensure it doesn't contain invalid data
defaultValue = sanitiseValue(defaultValue, schema?.[field], type)
// If we've already registered this field then keep some existing state // If we've already registered this field then keep some existing state
let initialValue = Helpers.deepGet(initialValues, field) ?? parsedDefault let initialValue = Helpers.deepGet(initialValues, field) ?? defaultValue
let initialError = null let initialError = null
let fieldId = `id-${Helpers.uuid()}` let fieldId = `id-${Helpers.uuid()}`
const existingField = getField(field) const existingField = getField(field)
@ -183,7 +177,9 @@
// Determine the initial value for this field, reusing the current // Determine the initial value for this field, reusing the current
// value if one exists // value if one exists
initialValue = fieldState.value ?? initialValue if (fieldState.value != null && fieldState.value !== "") {
initialValue = fieldState.value
}
// If this field has already been registered and we previously had an // If this field has already been registered and we previously had an
// error set, then re-run the validator to see if we can unset it // error set, then re-run the validator to see if we can unset it
@ -206,11 +202,11 @@
error: initialError, error: initialError,
disabled: disabled:
disabled || fieldDisabled || (isAutoColumn && !editAutoColumns), disabled || fieldDisabled || (isAutoColumn && !editAutoColumns),
defaultValue: parsedDefault, defaultValue,
validator, validator,
lastUpdate: Date.now(), lastUpdate: Date.now(),
}, },
fieldApi: makeFieldApi(field, parsedDefault), fieldApi: makeFieldApi(field),
fieldSchema: schema?.[field] ?? {}, fieldSchema: schema?.[field] ?? {},
}) })
@ -225,18 +221,9 @@
return fieldInfo return fieldInfo
}, },
validate: () => { validate: () => {
let valid = true return fields
let validationFields = fields .filter(field => get(field).step === get(currentStep))
.every(field => get(field).fieldApi.validate())
validationFields = fields.filter(f => get(f).step === get(currentStep))
// Validate fields and check if any are invalid
validationFields.forEach(field => {
if (!get(field).fieldApi.validate()) {
valid = false
}
})
return valid
}, },
reset: () => { reset: () => {
// Reset the form by resetting each individual field // Reset the form by resetting each individual field