Skip to content

Commit 3ed2d70

Browse files
committed
🔖 version bump
1 parent bcdb396 commit 3ed2d70

File tree

5 files changed

+152
-2
lines changed

5 files changed

+152
-2
lines changed

packages/publish/README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -913,6 +913,40 @@ return (
913913
)
914914
```
915915
916+
### `useTag`
917+
918+
Observes an entity, or world, for a tag and reactively updates when it is added or removed. Returns `true` when the tag is present or `false` when absent. Use this instead of `useTrait` for tags. For tracking the presence of non-tag traits, use `useHas`.
919+
920+
```js
921+
const IsActive = trait()
922+
923+
function ActiveIndicator({ entity }) {
924+
// Returns true if the entity has the tag, false otherwise
925+
const isActive = useTag(entity, IsActive)
926+
927+
if (!isActive) return null
928+
929+
return <div>🟢 Active</div>
930+
}
931+
```
932+
933+
### `useHas`
934+
935+
Observes an entity, or world, for any trait and reactively updates when it is added or removed. Returns `true` when the trait is present or `false` when absent. Unlike `useTrait`, this only tracks presence and not the trait's value.
936+
937+
```js
938+
const Health = trait({ amount: 100 })
939+
940+
function HealthIndicator({ entity }) {
941+
// Returns true if the entity has the trait, false otherwise
942+
const hasHealth = useHas(entity, Health)
943+
944+
if (!hasHealth) return null
945+
946+
return <div>❤️ Has Health</div>
947+
}
948+
```
949+
916950
### `useTraitEffect`
917951
918952
Subscribes a callback to a trait on an entity. This callback fires as an effect whenever it is added, removed or changes value without rerendering.

packages/publish/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "koota",
3-
"version": "0.5.2",
3+
"version": "0.5.3",
44
"description": "🌎 Performant real-time state management for React and TypeScript",
55
"license": "ISC",
66
"type": "module",

packages/publish/tests/core/query.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,4 +424,24 @@ describe('Query', () => {
424424
expect(entities[i].id()).toBe(i + 1);
425425
}
426426
});
427+
428+
it('cached query should return values after reset', () => {
429+
const movementQuery = cacheQuery(Foo);
430+
431+
const spawnEntities = () => {
432+
for (let i = 0; i < 100; i++) {
433+
world.spawn(Foo);
434+
}
435+
};
436+
437+
spawnEntities();
438+
const resultsBefore = world.query(movementQuery);
439+
440+
world.reset();
441+
spawnEntities();
442+
443+
const resultsAfter = world.query(movementQuery);
444+
445+
expect(resultsAfter.length).toBe(resultsBefore.length);
446+
});
427447
});

packages/publish/tests/core/relation.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,4 +232,29 @@ describe('Relation', () => {
232232
expect(person.has(Likes(apple))).toBe(false);
233233
expect(person.has(Likes(banana))).toBe(false);
234234
});
235+
236+
it('should keep wildcard trait when removing one of multiple relations to the same target', () => {
237+
const Likes = relation();
238+
const Fears = relation();
239+
240+
const person = world.spawn();
241+
const dragon = world.spawn();
242+
243+
// Person both likes and fears the dragon
244+
person.add(Likes(dragon));
245+
person.add(Fears(dragon));
246+
247+
// Wildcard(dragon) query should find person
248+
expect(world.query(Wildcard(dragon))).toContain(person);
249+
250+
// Remove only the Likes relation
251+
person.remove(Likes(dragon));
252+
253+
// Person should still fear the dragon
254+
expect(person.has(Fears(dragon))).toBe(true);
255+
expect(person.has(Likes(dragon))).toBe(false);
256+
257+
// Wildcard(dragon) should still find person because Fears(dragon) remains
258+
expect(world.query(Wildcard(dragon))).toContain(person);
259+
});
235260
});

packages/publish/tests/react/trait.test.tsx

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { createWorld, trait, universe, type Entity, type TraitRecord, type World
22
import { render } from '@testing-library/react';
33
import { act, StrictMode, useEffect, useState } from 'react';
44
import { beforeEach, describe, expect, it } from 'vitest';
5-
import { useTrait, useTraitEffect, WorldProvider } from '../../react';
5+
import { useHas, useTag, useTrait, useTraitEffect, WorldProvider } from '../../react';
66

77
declare global {
88
var IS_REACT_ACT_ENVIRONMENT: boolean;
@@ -13,6 +13,7 @@ global.IS_REACT_ACT_ENVIRONMENT = true;
1313

1414
let world: World;
1515
const Position = trait({ x: 0, y: 0 });
16+
const IsTagged = trait();
1617

1718
describe('useTrait', () => {
1819
beforeEach(() => {
@@ -177,6 +178,76 @@ describe('useTrait', () => {
177178
});
178179
});
179180

181+
describe('useTag', () => {
182+
beforeEach(() => {
183+
universe.reset();
184+
world = createWorld();
185+
});
186+
187+
it('reactively returns a boolean for a trait', async () => {
188+
const entity = world.spawn(IsTagged);
189+
let isTagged: boolean | undefined;
190+
191+
function Test() {
192+
isTagged = useTag(entity, IsTagged);
193+
return null;
194+
}
195+
196+
await act(async () => {
197+
render(
198+
<StrictMode>
199+
<WorldProvider world={world}>
200+
<Test />
201+
</WorldProvider>
202+
</StrictMode>
203+
);
204+
});
205+
206+
expect(isTagged).toBe(true);
207+
208+
await act(async () => {
209+
entity.remove(IsTagged);
210+
});
211+
212+
expect(isTagged).toBe(false);
213+
});
214+
});
215+
216+
describe('useHas', () => {
217+
beforeEach(() => {
218+
universe.reset();
219+
world = createWorld();
220+
});
221+
222+
it('reactively returns a boolean for any trait', async () => {
223+
const entity = world.spawn(Position);
224+
let hasPosition: boolean | undefined;
225+
226+
function Test() {
227+
hasPosition = useHas(entity, Position);
228+
return null;
229+
}
230+
231+
await act(async () => {
232+
render(
233+
<StrictMode>
234+
<WorldProvider world={world}>
235+
<Test />
236+
</WorldProvider>
237+
</StrictMode>
238+
);
239+
});
240+
241+
expect(hasPosition).toBe(true);
242+
243+
await act(async () => {
244+
entity.remove(Position);
245+
});
246+
247+
expect(hasPosition).toBe(false);
248+
});
249+
});
250+
180251
describe('useTraitEffect', () => {
181252
beforeEach(() => {
182253
universe.reset();

0 commit comments

Comments
 (0)