v2.0.0 is a breaking release. The core emitter API stays almost the same. The React integration was rewritten as a single hook in its own subpath.
If you only use CozyEvent without React, most of your code keeps working. Check Behavior changes anyway.
- Install: no more
latest-lighttag - React import path changed
CozyEventProviderremoved: use a module-level emitteruseCozyEventsignature changed- Namespaces removed
- Multiple providers /
idoption removed - Instance registry removed
CozyEventContextand the global instance removedon/oncenow return an unsubscribe function (non-breaking)oncelisteners can be removed withoff- Behavior changes (core)
- TypeScript: typed events
- Checklist
v1 had a React-free "light" build under a separate dist-tag. In v2 the main package contains no React code, so there is only one install command.
Before
npm install cozyevent@latest-lightAfter
npm install cozyeventReact is now an optional peer dependency (>=16.8.0). You only need it if you import cozyevent/react.
The main entry cozyevent no longer exports anything related to React. The hook is available only from cozyevent/react.
Before
import { CozyEvent, useCozyEvent, CozyEventProvider } from 'cozyevent';After
import { CozyEvent } from 'cozyevent';
import { useCozyEvent } from 'cozyevent/react';In v2 there is no provider or context. Create an emitter in a module and import it wherever you need it.
Before
import { CozyEventProvider, useCozyEvent, getCozyEventInstanceById } from 'cozyevent';
const Sender = () => (
<button onClick={() => getCozyEventInstanceById('default')?.emit('test-event', 'Hello!')}>
Emit
</button>
);
const Receiver = () => {
useCozyEvent('test-event', (message) => alert(message));
return <div>Listening...</div>;
};
export const App = () => (
<CozyEventProvider>
<Sender />
<Receiver />
</CozyEventProvider>
);After
// events.ts
import { CozyEvent } from 'cozyevent';
// List every event you use. The later examples in this guide use these names too.
export const appEvents = new CozyEvent<{
'test-event': string;
message: unknown;
other: number;
'auth:user-login': unknown;
'global-event': unknown;
}>();// App.tsx
import { useCozyEvent } from 'cozyevent/react';
import { appEvents } from './events';
const Sender = () => (
<button onClick={() => appEvents.emit('test-event', 'Hello!')}>Emit</button>
);
const Receiver = () => {
useCozyEvent(appEvents, 'test-event', (message) => alert(message));
return <div>Listening...</div>;
};
export const App = () => (
<>
<Sender />
<Receiver />
</>
);If you passed a custom instance (<CozyEventProvider instance={myEmitter}>), pass myEmitter straight to useCozyEvent instead.
If you really need a different emitter for each subtree (for example in tests), put the emitter in your own React context and read it with useContext before calling useCozyEvent(emitter, ...). CozyEvent no longer ships a context for this.
| v1 | v2 | |
|---|---|---|
| Signature | useCozyEvent(eventName, callback, { id?, namespace? }) |
useCozyEvent(emitter, event, listener) |
| Emitter source | nearest provider, id from the registry, or the global instance |
the emitter argument |
| Return value | the CozyEvent instance |
void |
| Runtime validation | threw on an empty event name or a non-function callback | none (TypeScript checks the types) |
| Resubscribe on re-render with an inline callback | yes (bug) | no, the latest listener is kept in a ref |
Before
const emitter = useCozyEvent('message', (data) => console.log(data));
emitter.emit('other', 1);After
import { appEvents } from './events';
useCozyEvent(appEvents, 'message', (data) => console.log(data));
appEvents.emit('other', 1);In v1, namespace was joined to the event name as `${namespace}:${eventName}`. In v2 you put the prefix in the event name yourself. The event name ends up exactly the same, so emitters that already emit 'auth:user-login' keep working.
Before
useCozyEvent('user-login', (data) => console.log('Logged in:', data), {
namespace: 'auth',
});After
useCozyEvent(appEvents, 'auth:user-login', (data) => console.log('Logged in:', data));Before
const App = () => (
<>
<CozyEventProvider id="auth">
<AuthModule />
</CozyEventProvider>
<CozyEventProvider id="notifications">
<NotificationModule />
</CozyEventProvider>
</>
);
const AuthModule = () => {
useCozyEvent('login', (user) => console.log('User:', user), { id: 'auth' });
return <div>Auth</div>;
};After
// events.ts
import { CozyEvent } from 'cozyevent';
export const authEvents = new CozyEvent<{ login: User }>();
export const notificationEvents = new CozyEvent<{ 'new-message': string }>();import { useCozyEvent } from 'cozyevent/react';
import { authEvents } from './events';
const App = () => (
<>
<AuthModule />
<NotificationModule />
</>
);
const AuthModule = () => {
useCozyEvent(authEvents, 'login', (user) => console.log('User:', user));
return <div>Auth</div>;
};registerCozyEventInstance and getCozyEventInstanceById are gone. Export your emitter from a module instead. ES modules are singletons, so every importer gets the same instance.
Before
import { CozyEvent, registerCozyEventInstance, getCozyEventInstanceById } from 'cozyevent';
registerCozyEventInstance('custom', new CozyEvent());
// somewhere else
getCozyEventInstanceById('custom')?.emit('event', 'Hello!');After
// custom-events.ts
import { CozyEvent } from 'cozyevent';
export const customEvents = new CozyEvent();// somewhere else
import { customEvents } from './custom-events';
customEvents.emit('event', 'Hello!');If your IDs really are dynamic, keep your own Map<string, CozyEvent>:
const emitters = new Map<string, CozyEvent>();
export const getEmitter = (id: string) => {
let e = emitters.get(id);
if (!e) emitters.set(id, (e = new CozyEvent()));
return e;
};CozyEventContext and the implicit global fallback instance (used by useCozyEvent when no provider was found; it was never exported) no longer exist.
Before
import { useContext } from 'react';
import { CozyEventContext } from 'cozyevent';
const emitter = useContext(CozyEventContext);
useCozyEvent('global-event', (data) => console.log(data)); // fell back to the global instanceAfter
import { appEvents } from './events'; // your own module-level emitter
useCozyEvent(appEvents, 'global-event', (data) => console.log(data));In v1, on and once returned void. Existing code keeps working, and you can now remove a listener without keeping a named reference to it.
Before
const handler = (msg: string) => console.log(msg);
emitter.on('chat', handler);
// later
emitter.off('chat', handler);After (either style works)
const unsubscribe = emitter.on('chat', (msg) => console.log(msg));
// later
unsubscribe();Calling the unsubscribe function more than once is safe. It never removes another registration of the same function.
In v1, once wrapped your listener internally, so off(event, listener) silently did nothing. In v2 it removes the listener.
Before (v1 bug)
const onReady = () => console.log('ready');
emitter.once('ready', onReady);
emitter.off('ready', onReady); // did NOT remove it
emitter.emit('ready'); // logged "ready"After
const onReady = () => console.log('ready');
emitter.once('ready', onReady);
emitter.off('ready', onReady); // removed
emitter.emit('ready'); // nothing
// or
const cancel = emitter.once('ready', onReady);
cancel();These changes are unlikely to affect typical code, but check them if you rely on edge cases.
In v1, off(event, fn) removed every registration of fn. In v2 it removes one, the most recently added one. This matches node:events.
Before
emitter.on('tick', fn);
emitter.on('tick', fn);
emitter.off('tick', fn); // v1: both removedAfter
emitter.on('tick', fn);
emitter.on('tick', fn);
emitter.off('tick', fn); // v2: one removed, fn still runs once per emit
emitter.off('tick', fn); // now none
// or remove the whole event:
emitter.removeAllListeners('tick');In v1, an empty string was falsy and cleared all events. In v2 only removeAllListeners() with no argument (or undefined) clears everything.
emitter.removeAllListeners(''); // v1: everything | v2: only the '' event
emitter.removeAllListeners(); // both: everythingIn v1, event names that exist on Object.prototype crashed:
emitter.emit('toString'); // v1: TypeError "forEach is not a function"
emitter.on('constructor', cb); // v1: TypeError "push is not a function"In v2 these, along with '__proto__', 'hasOwnProperty' and 'valueOf', behave like any other event name.
In v1, emitAsync looked up listeners inside the microtask. If they had been removed in the meantime, it threw Cannot read properties of undefined, and listeners added in the meantime would run. In v2 the listener list is captured when emitAsync is called:
emitter.on('save', handler);
emitter.emitAsync('save', doc);
emitter.off('save', handler);
// v1: throws inside the microtask
// v2: handler still runs once with doc (it was registered when emitAsync was called)- Listeners added during an emit are not called in that emit.
- Listeners removed during an emit that have not run yet still run in that emit.
In v1, a once listener was removed after it ran, so a listener that threw stayed registered, and a nested emit of the same event called it again. In v2 it is removed before its body runs, and it runs at most once.
Listeners get a single payload argument, as in v1. Extra arguments passed to emit or emitAsync are dropped. To send several values, pass an object.
v1 stored listeners in this._events. That was never public API, and v2 does not have it. If a subclass read _events, keep track of the listeners yourself instead.
v2 reserves four internal names: _e (listener storage), _k (the name of the event emptied last) and _r (a private helper) and _T (a type-only marker that emits no JavaScript). A subclass must not define members with those names. TypeScript reports an error if you redeclare one.
Unchanged from v1: listeners are called as plain functions, so this is undefined in strict-mode code (ES modules, classes, TypeScript output) and globalThis in sloppy-mode scripts. It is never the emitter. Use arrow functions, or bind the listener yourself.
This is new and optional. CozyEvent takes an event map:
const auth = new CozyEvent<{ login: User; logout: void }>();
auth.on('login', (user) => user.name); // user: User
auth.emit('logout');
// auth.emit('login', 42); // type errorv1 accepted a per-call generic like on<T>(...)/emit<T>(...). Those generics are gone. Put the types in the class type parameter instead. new CozyEvent() without a type parameter still accepts any event name and payload.
-
npm install cozyevent(drop@latest-light). - Import
useCozyEventfromcozyevent/react. - Replace
<CozyEventProvider>with a module-levelnew CozyEvent(). - Change
useCozyEvent(name, cb, opts)touseCozyEvent(emitter, name, cb), and putnamespace:into the name. - Replace
getCozyEventInstanceById/registerCozyEventInstancewith exported emitters. - Remove uses of
CozyEventContext, and stop using the return value ofuseCozyEvent. - Check any code that calls
offexpecting it to remove duplicate registrations, or that callsremoveAllListeners(''). - Remove any access to
_events, and make sure subclasses do not define_e,_k,_ror_T. - Make sure no listener relies on
this.