Fandom Developers Wiki

Preact is a JavaScript library that lets you build interactive user interfaces with a reactive programming model. With MediaWiki hooks and some helper code that lets you write and use components and hooks using ES3 without much hassle, this library can be used on new Dev scripts.

It's ideal for scripts that take complete control of a piece of the document with an interactive interface with multiple subtle ways of updating the document. Declarative components with internal state make keeping the interface updated with your script's internal data easy.

For scripts that will not have lots of interactivity and only need a simple way of building DOM elements, you can look into Dorui or UI-js. Do keep in mind, though, that Preact and element-creating libraries are not competing; you can use both, if you need to create simple elements (for example, inserting to the toolbar) and for the interface. You just have to make sure not to mix them together in the same tree.

Importing

importArticle({
    type: 'script',
    article: 'u:dev:MediaWiki:Preact.js'
});

mw.hook('dev.preact').add(function(preact) {
    // Your code here
    // `preact` is an alias to `window.dev.preact`
    // You will likely end up aliasing a handful of methods and properties from `preact`
    // More of this will be discussed in the examples section
});

Preact vs. React

React and Preact are very similar libraries that are largely interoperable. This script uses the latter due to its smaller footprint, closer to HTML attributes (for instead of htmlFor, class instead of className, etc.), and, by default, using an array of children in props instead of varargs.

Learning React

Knowledge and experience is largely interchangeable between React and Preact. The actual differences are relatively few and a lot of them make no difference for regular usage of this library. However, you should still be somewhat familiar with them.

This script will probably not be very useful to you unless you have prior experience working with a React application. It's suggested you first go through a guide or the documentation.

  • Here is the React tutorial, where you build a tic-tac-toe application. I do not recommend this tutorial, and instead, advise you to start with the main concepts introduction:
  • React main concepts guide, a 12-chapter guide introducing you from the smallest hello world component, to updating the interface in-place by calling render multiple times, to state and complex problems like form management. This is the ideal way of being introduced to React, though it uses largely class components throughout for stateful UIs. You should supplement it with the introduction to hooks, and you should have a solid foundation to go off.
  • Here is the index of the React documentation, where everything about the library can be found. You can learn more about features exposed in this library by looking at its official documentation.
  • Here is the getting started guide for Preact, however it doesn't have much useful information. You may choose to skip to the "Essentials" section, where features like Components, Hooks, Forms, References, and Context are explained. Do keep in mind that class components are highly discouraged and you should favor hooks instead.

Do know that using either React or Preact in Dev scripts will be different due to the restrictions imposed by the ResourceLoader. Your script is likely to have a single file where all components reside, and you're restricted to ES3 features, so without a transpiler (webpack, rollup, etc.) you won't be able to use JSX, arrow functions, or destructuring. However, the most common uses for destructuring (useState and useReducer) have alternatives that don't need any. These will be discussed in the documentation that follows, under Hooks.

Documentation

This section will document all the features that the library exports. It will only provide cursory explanations of features, providing a link to the official documentation, and go into detail if it differs from the original or is a new addition.

The features will be explained for each property that the library exposes. Keep in mind that the library is at window.dev.preact and is fired as the argument of the dev.preact MW hook.

.render(element, container)

Documentation: render

This method renders a React tree into a container element. The first parameter must be a React element, created by calling h() with a Component or a tag name.

preact.render(
    h(App),
    document.getElementById('content')
);

.createElement(element, props)

Documentation: createElement
Alias: .h(element, props)

Creates an element from a tag name or a Component function. It's advised to use the shorter variant .h() instead, and keep a variable referencing it so creating elements is very short.

The type signature differs from the one used in React. While vararg children do work, from the 3rd argument onwards, it's highly discouraged. You should pass children directly in the props object as an array. You can also use child if you will only be passing a single child element.

Remember that what you must pass as children are React elements, not Node objects or Component functions. You can pass strings, though, and they'll be converted to a text node in the document.

If creating normal tags by passing a string as the first argument is ugly to you, do consider using the shorthand .tags object. You may prefer it, and it's the recommended way of creating elements with this library. Components still have to be rendered via createElement, however.

preact.render(
    // Creating a div tag
    h('div', {
        // Giving it an attribute
        class: 'my-app',
        children: [
            // Creating a component with props
            h(Component, {
                // Components can take whatever props they want
                value: 27,
                // They can even take children
                // This property will not be passed as `child`,
                // but as a single-element array `children`
                child: 'Strings for text nodes work!'
            })
        ]
    })
    container
);

.cloneElement(element, props)

Documentation: cloneElement
Documentation: cloneElement (preact)

Clones a React element. This lets you take an existing element and extend it with your own props, or replace its children. You should use props.children to replace children instead of the third argument onwards. No significant changes from the original otherwise.

.createContext(defaultValue)

Documentation: createContext

Creates a Context object that you can insert into your component tree with MyContext.Provider and consume with useContext. No significant changes from the original.

.toChildArray(children)

Documentation: toChildArray (preact)

Utility function that lets you convert props.children into a flat array of children. This will wrap single children in an array and flatten the infinitely nested arrays that might be inside. No significant changes from the original.

Fragment

Documentation: Fragments

The Fragment class for returning multiple elements from components or otherwise grouping react elements. You should probably use the helper .frag instead. Other than that, no significant changes from the original.

.frag(children)

This helper function lets you avoid writing h(Fragment, { children: [...] }). Since JSX is not available, you cannot use <> for fragments, so this is hopefully a reasonable alternative to use.

function MyComponent() {
    return preact.frag([
        'Hello there',
        h(SubComponent)
    ]);
}

.tags

This is a special object that provides you with shorthands for creating HTML tags. It's a 🗲「Proxy」🗲, and properties you access in it will be functions that create an element named after the property you accessed. For example, tags.div(props) will be the equivalent of calling h('div', props).

tags.div({
    child: 'This really is not that weird, I promise'
});

.memo(Component, cmp)

Documentation: memo

Wraps your function component and only re-renders it whenever the comparison function returns true. When the comparison function is not provided, a shallow props comparison is made, and if any properties between the previous and current props are different or missing, it will return true and re-render the component.

See optimizing performance in the React docs, and this blog post on when to use memo.

Hooks

Hooks are how you add state and side effects to your function components. They're imperative to making a usable interface with the library. Keep in mind that the useState and useReducer hooks deviate from their standard versions. The other hooks are equivalent to the alternatives.

There are other hooks worth talking about: useImperativeHandle and useDebugValue. These are not exposed by the library, even though, internally, they're available. This is because they depend on features the library is not bundled with. useImperativeHandle requires forwardRef, a feature from preact/compat, and useDebugValue needs a dev build of preact to be useful.

useErrorBoundary is another interesting hook. This is because it's Preact-specific, there is no React hook for it because in React it must be done with classes. Classes are greatly discouraged in this library, so this proves awfully convenient for Preact.

useState(initialState)

Documentation: useState

The useState hook. You should be very familiar with it. However, you cannot use destructuring in dev scripts. As such, the useState function that's exposed by this library is slightly different.

useState returns an object with two properties: value and set. They're the equivalent of the first and second value of the returned array in useState, respectively.

This means that instead of destructuring into two variables, you simply have one state variable with a setter and a value property. As always, the setter function's identity is stable, and so is the state value if it wasn't updated with the setter function.

However, the identity of the object itself, is not stable. You should not use it for, say, the dependency argument in useEffect. You should use state.value instead, and it will work.

function MyComponent() {
    var counter = useState(0);

    // {Note how we use counter.value in the dependency list, not counter
    // As always, the setter function's identity is stable,
    // so you can omit it from the dependency list even if you use it
    useEffect(function() {
        console.log('The value of counter changed!');
        console.log('The new value is: ' + counter.value);
    }, [counter.value]);

    return ui.span({
        child: 'The current count is ' + counter.value + '. Click me to increase it!',
        onClick: function() {
            counter.set(counter.value + 1);

            // You can also use the callback method
            // This is preferred when the new state depends on the previous
            // In this case, this is the more appropriate way of setting state
            // But in an example this small, either one works just fine
            // Just don't do both at once as shown here!
            counter.set(function(count) {
                return count + 1;
            });
        }
    });
}

useReducer(reducer, initialArg, init)

Documentation: useReducer

The useReducer hook. You can learn more about it in the documentation above.

Just like useState, you don't have access to destructuring, so this function returns an object { state, dispatch } instead. state and dispatch hold the same stability promises as the regular useReducer, but the object itself does not. Keep it in mind and you won't have trouble with this.

useEffect(effect, deps)

Documentation: useEffect

The useEffect hook, the main way of introducing side effects to your components rendering. You can do data fetching in these, setting up intervals, subscriptions, or anything else. You can also return a function that will be called as cleanup. Make sure to read the documentation, this is a very important hook.

useMemo(fn)

Documentation: useMemo

The useMemo hook. No significant changes from the original.

useCallback(fn)

Documentation: useCallback

The useCallback hook. Useful for preventing re-renders in subcomponents! No significant changes from the original.

useRef(initialValue)

Documentation: useRef

The useRef hook. Very useful for accessing internal DOM nodes returned by your components, or keeping around any state that you don't need your component to re-render to updates. No significant changes from the original.

useContext(fn)

Documentation: useContext

The useContext hook. For avoiding drilling down state too much. Sorry you can't use redux yet! No significant changes from the original.

useLayoutEffect(fn)

Documentation: useLayoutEffect

The useLayoutEffect hook. Called synchronously after DOM mutations are flushed to the document itself. You may use this hook to update the document, recalculate some value based on the new document (for example, an element's new scroll height to update its scroll position), and synchronously re-render inside the effect. This is done before the browser paints to the screen, so the user does not notice the inconsistent in-between state. This comes at the expense of being more invasive into Preact's rendering pipeline and giving it less freedom with when to execute your code, potentially adding jank or slowing down your interface. No significant changes from the original.

useErrorBoundary(callback)

Documentation: useErrorBoundary

The useErrorBoundary hook. This is a special hook from Preact, that replaces React error boundaries and lets you use them from hooks. When used on a component, errors in its subtree will be propagated up to and caught by the error boundary. The callback will be called with the error, if provided.

The original hook returns a pair of elements [error, resetError], which means that this hook too was modified to return an object instead. It now returns an object with the shape { error, reset }.

function MyErrorBoundary(props) {
    var boundary = useErrorBoundary(function(error) {
        console.error('Oh no, an error occurred!');
        console.error('Log it somewhere');
        console.error(error);

        const reset = confirm('An error was thrown in the app. Do you wish to continue with the unstable interface?');

        if (reset) {
            boundary.reset();
        }
    });

    if (boundary.error) {
        return tags.div({
            child: 'An error occurred. ' + boundary.error.message
        });
    }

    return props.children;
}

Examples

Example counter script

This is a short program that will give you a rough idea of the things a script using Preact may do. It will be wrapped around by an IIFE, it will have a few placeholder variables as shorthands (preact, h, tags, and useState). It will also call preact.render with an App component, and listen to the dev.preact MW hook. Finally, it imports the library itself.

(function() {
    // Declare our shorthands into the `dev.preact` object
    var preact;
    var h;
    var tags;
    var useState;

    // Our Counter component!
    // It takes a `start` prop, which is where the counter will start,
    // and where it will be reset when the user clicks the "Reset" button
    function Counter(props) {
        var count = useState(props.start);

        return tags.div({
            children: [
                'The counter is at: ' + count.value,
                tags.button({
                    child: 'Increase me',
                    onClick: function() {
                        count.set(count.value + 1);
                    }
                }),
                tags.button({
                    child: 'Decrease me',
                    onClick: function() {
                        count.set(count.value - 1);
                    }
                }),
                tags.button({
                    child: 'Reset me',
                    onClick: function() {
                        count.set(props.start);
                    }
                })
            ]
        });
    }

    // This is the root App component, not really a lot going on here
    // But you can add a Counter from here, or multiple
    // They will have independently-managed states
    function App() {
        return tags.div({
            children: [
                'This is the app component',
                h(Counter, {
                    start: 5
                }),
                h(Counter, {
                    start: 10
                })
            ]
        });
    }

    function init() {
        var container = document.getElementById('content');

        container.innerHTML = '';

        preact.render(
            h(App),
            container
        );
    }

    mw.hook('dev.preact').add(function(_preact) {
        // Assign to our shorthands
        preact = _preact;
        h = preact.h;
        tags = preact.tags;
        useState = preact.useState;

        init();
    });

    importArticles({
        type: 'script',
        articles: [
            'u:dev:MediaWiki:Preact.js'
        ]
    });
})();
Text above can be found here (edit)