mojang / ore-ui Goto Github PK
View Code? Open in Web Editor NEW๐ Building blocks to construct game UIs using web tech.
Home Page: https://react-facet.mojang.com/
License: MIT License
๐ Building blocks to construct game UIs using web tech.
Home Page: https://react-facet.mojang.com/
License: MIT License
Map what components needs this. Sync with art and sound design project.
e.g., in this example:
import { Map, useFacetState } from "@react-facet/core";
import { sharedFacet, sharedDynamicSelector, useSharedFacet } from "@react-facet/shared-facet";
interface Member {
name: string
rank: number
health: number
}
const membersArray = [
{
name: "Zeus",
rank: 3,
health: 90
},
{
name: "Demeter",
rank: 2,
health: 100
},
{
name: "Poseidon",
rank: 1,
health: 85
}
];
const membersFacet = sharedFacet<Member[]>("data.team", membersArray);
const memberNameSelector = sharedDynamicSelector((index: number) => [
(member) => member[index].name,
[membersFacet]
]);
const memberRankSelector = sharedDynamicSelector((index: number) => [
(member) => member[index].rank,
[membersFacet]
]);
const memberHealthSelector = sharedDynamicSelector((index: number) => [
(member) => member[index].health,
[membersFacet]
]);
const Member = ({ index }: { index: number }) => {
return (
<span>
<b>Name: </b><fast-text text={useSharedFacet(memberNameSelector(index))}/>
<br/>
<b>Rank: </b><fast-text text={useSharedFacet(memberRankSelector(index))}/>
<br/>
<b>HP: </b><fast-text text={useSharedFacet(memberHealthSelector(index))}/>
<br/>
<hr/>
</span>
);
};
export const Team = () => {
// works (as expected)
// const [membersFacetState] = useFacetState<Member[]>(membersArray);
// does not
const membersFacetState = useFacetState(membersFacet);
return (
<Map array={membersFacetState}>
{(_, index) => <Member index={index} />}
</Map>
);
};
the type error returned on Map's "array" prop:
TS2322: Type
[Facet<SharedFacet<Member[]>>, Setter<SharedFacet<Member[]>>]
is not assignable to type Facet<unknown[]>
Map.d.ts(4, 5): The expected type comes from property array which is declared here on type IntrinsicAttributes & MapProps<unknown>
I've tried digging through the docs, attempting to map from a SharedFacet to a Facet (couldn't seem to get that working), so figured I'd ask here. Thanks!
Right now, if we run:
useFacetEffect(() => { console.log('I will never fire' }, [], [])
The effect will never fire, because useFacetEffect
expects for all facets to resolve to a value before firing the effect. In this case, there are no facets to resolve in the second dependency array, so it will just never fire at all.
In a way this scenario doesn't make sense (we should use useEffect
instead) but since there's no way to enforce that empty arrays are not allowed, we should fail gracefully. Instead of never firing, useFacetEffect
with an empty facet array should just behave in exactly the same way as a regular useEffect
. This, we think, would be the expected behavior.
This issue applies also to a similar scenario in useFacetCallback
. We should fix that one as well in the same way.
Moving forward, we don't want to mix and match methods and data in shared facets. We want to think of data facets as pure data, much like Redux state, and method facet as flat lists of available API calls from the remote.
Additionally, MethodSharedFacetValue
s could automatically do a .bind
on the methods it includes, to avoid issues with selecting individual methods from the facet and losing the this
reference.
Steps to reproduce
Edit this page
at the bottomExpected
Redirected to edit the file: https://github.com/Mojang/ore-ui/edit/documentation/react-facet/docs/goals.md
Actual
Redirected to this 404: https://github.com/mojang/ore-ui/edit/main/docs/goals.md
Currently, the <Map>
component will send a Facet containing each item value and a number that is the index of that item to the function that it receives as children
. So:
<Map array={useFacetWrap(['alpha', 'beta', 'gamma'])}>
{(item, index) => { /* ... */ }}
</Map>
The fact that this index
is sent as the second argument is currently not documented and not tested anywhere.
We could investigate using tools such as https://github.com/changesets/changesets
We could also check if it is worth using something like https://turborepo.org/. They also have a fantastic list of complementary tools we can look at https://turborepo.org/docs/guides/complimentary-tools
Includes the Switch SDK 14 update.
As of #34, when DeferredMountWithCallback
is used, all deferred rendering is paused until that callback is invoked.
To avoid blocking all rendering, we might want to place the callback in a second queue for notifying when all rendering is complete, but avoid blocking the start of render of the next component. This should be optional, so that we can also block when waiting for render to be completed before starting the next.
This change does not break API.
We should introduce a new property for signalling when we explicitly want to block further rendering, it could look something like this:
<DeferredMountWithCallback isBlocking>
<ComponentThatShouldBeWaitedFor />
</DeferredMountWithCallback>
There is room to improve a bit further our strategy regarding equality checks. Right now we have a "default equality check" that is applied everywhere, but this default implementation adds a runtime type check on every useFacetMap
and useFacetMemo
we add.
The goal here is to remove the overhead and align the behavior of equality checks to what is expected from "vanilla React". React has no memoization by default, and we can probably lean on the same premise.
useFacetMap
and useFacetMemo
Change to have NO equality checks set by default, and if needed it should be added explicitly by the user. This will make our map implementation much closer to React's component (React re-renders a component if called with the same values).
useFacetState
Here, we have two options:
With either approach we would still support being explicit about which equality check to use (as it is today).
useFacetEffect
The proposal would be to maintain the behavior we have today, but as an "effect" of the previous changes it would mean for users our useFacetEffect
would behave differently from useEffect
.
Given the equality checks would be removed from the maps, effects would always be triggered, even for updates with the same reference.
We could extend the API to support equality checks, but given we can have multiple facets as a dependency, it would become quite verbose to setup any checks.
So, to prevent effects being fired, users would need to add equality checks on outer layers (ex: useFacetMap
), if needed.
dom-fiber
and dom-components
To support this change, our custom renderer would need to start performing equality checks on each prop to know for sure if the value of a facet have changed. The benefit here is that we would know for sure the type of the Facet and can do strict equality checks.
Is there anything that stops from updating to the last version of react? Current one is 16 which is pretty old
Example:
const [valueFacet, setValue, valueRef] = useFacetState(0)
This would be equivalent to doing
const [valueFacet, setValue] = useFacetState(0)
const valueRef = useFacetRef(valueFacet)
But then there would be no need for useFacetRef
do use a useFacetEffect
, instead updating the ref could happen in the setter. This would help with performance, and make the type of the ref cleaner (since it can't be NO_VALUE
in this case)
In a scenario where a function that is listening on a facet is calling another function created with useFacetCallback
, the value of the same facet inside the second callback might be outdated.
For example:
const facet = createFacet({ initialValue: 'old value' })
function Comp() {
// Since this is run on useLayoutEffect, it will
// add the
const callback = useFacetCallback((value) => value, [], [facet])
// Since this is run on render, it will add the
// `.observe` subscription first
useMemo(() => {
facet.observe((value) => {
console.log('here be new value: ', value)
console.log('...and here is still old value: ', callback())
})
})
return null
}
facet.set('new value')
In some scenarios you need to keep track of more than one facet in one hook like useFacetEffect()
, however the downside is you never really know which one of them triggered the current run of the effect, one option is is to split the effect, but sometimes that's not feasible depending on if the logic actually depends on more than one facet at the same time and they're not unrelated.
The hook could accept a callback function overload whereby if the callback function has one more parameter than the facets passed to the hook it would keep track of the values and give you a perhaps meta
object (name pending) with simple booleans for the facet's position in the dependency array which indicate which facet changed this run.
This should be an opt-in approach (by expecting an extra parameter in a X facet dependency hook), so we don't have to pay that cost in normal scenarios which are the abundant use-case for those hooks.
useFacetEffect(
(firstFacetValue, secondFacetValue, meta) => {
if(meta.changed[0]) {
// Do something if firstFacetValue is the facet that changed triggering this effect
}
if(meta.changed[1]) {
// Do something if secondFacetValue is the facet that changed triggering this effect
}
},
[],
[firstFacet, secondFacet],
)
I hope to use oreui in the vue project.
Example:
type UserDataProps = {
name: FacetProp<string>
middlename?: FacetProp<string | undefined>
}
const UserData = ({ name, middlename }: UserDataProps) => {
const nameFacet = useFacetWrap(name)
const middlenameFacet = useFacetWrap(middlename)
return (
<div>
<p>Name: <fast-text text={nameFacet} /></p>
<With data={middlenameFacet}>
{(middlename) => <p>Middlename: <fast-text text={middlename} /></p>}
</With>
</div>
)
}
The current component that supports a similar use case, Mount
, has the defect that TypeScript is not able to tell when the data used inside it is defined. It cannot use the when
clause to refine the type. In particular, the following code will lead to an annoying type error (since you as a developer know the code is correct, but TypeScript doesn't)
type UserDataProps = {
name: FacetProp<string>
middlename?: FacetProp<string | undefined>
}
const UserData = ({ name, middlename }: UserDataProps) => {
const nameFacet = useFacetWrap(name)
const middlenameFacet = useFacetWrap(middlename)
return (
<div>
<p>Name: <fast-text text={nameFacet} /></p>
<Mount data={useFacetMap((middlename) => middlename != null, [],[middlenameFacet])}>
<p>Middlename: <fast-text text={middlenameFacet} /></p>
{/* Since TypeScript cannot know that `middlenameFacet` now holds a `string` for sure and still thinks that
it could be `string | undefined`, it will complain. The only way to fix this is to extract a new component
or with a type assertion. Neither is good */}
</With>
</div>
)
}
The main thing being that since @react-facet breaks out of the norms of React, when you access the children
prop it will not have the data you expect, because it will be out of sync with what actually gets rendered.
Can I help with coding make me member please I love coding
Shared facets are defined statically at a global level using the sharedFacet
function as a combination of a name
and a type
. As an example from our documentation:
export const userFacet = sharedFacet<UserFacet>('data.user', {
username: 'Alex'
signOut() {},
})
We want to take the existing concept of a name
and expand it to be more of a path
to look into a big global state object within the game engine.
Thinking about the previous example, the name data.user
could be interpreted the as the path to the contents of the user in the object below:
{
data: {
user: {}
}
}
This is really interesting when we consider that we could build a path dinamically in a React component to point to an specific instance of an object. Let's say we have a list of creatures in a game:
{
entity: {
hostile: {
'123': { name: 'creeper' },
'456': { name: 'piglin' }
}
}
}
And we want to subscribe to changes only to the creeper entity. With the current static definition of shared facets this could only be achieved via a dynamicSelector
, but then every selector would be notified when any entity changes. So we propose that we remove sharedFacet
as a function and instead have our API for subscribing to a shared facet to be completely handled in a hook by taking both the path
and the type:
const entityFacet = useSharedFacet<Entity>(['entity', 'hostile', '123'])
cc @xaviervia can you add some notes about it?
We could still have a "global definition" of a facet, but this would be instead achieved via a hook:
--- export const userFacet = sharedFacet<UserFacet>('data.user')
+++ export const useUserFacet = useSharedFacet<UserFacet>(['data', 'user'])
This change will remove/deprecate the APIs for:
sharedFacet
sharedSelector
sharedDynamicSelector
In favor of using hooks and useFacetMap
or useFacetMemo
to achieve the same functionality.
It is important to assess if there are any risks on removing these APIs. And although we would be removing them from the open-source packages, they could still be kept implemented internally by consumers of our packages (if we need to provide any backwards compatibility).
When requesting a shared facet to the engine we need to make sure we only make one request for a given "path", even if multiple places are subscribing to it. This is currently achieved by having a single instance for each shared facet via memoisation in the sharedFacet
definition.
This behavior would need to be re-implemented as part of the useSharedFacet
hook.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.