10.0.0 • Published 4 years ago

@yonabenreuven/socket.io-react v10.0.0

Weekly downloads
-
License
ISC
Repository
github
Last release
4 years ago

socket.io-react

socket io with react using hooks and context;

if you don't know what socket.io is you can read about it here.

installation:

if the installDependencies.sh script doesn't run automatically then run it your self

Server Side

example for how the server.js would loop is in serverExample.js

Add to your boot function in server.js file:

boot(app, __dirname, (err) => {
    if (err) throw err;
    if (require.main === module) {
        // OPTIONS for socket: you can add { transports: ["websocket", "xhr-polling"] };
        // this means you'll be using websocket instead of polling (recommended);
        // NOTE you need to have the same transports in the client too;
        const options = { transports: ["websocket", "xhr-polling"] }; // ! Not required !
        // you can read more about the options here: https://socket.io/docs/server-api/

        // Here we need to add the Socket to our server, like so: require('socket.io')(SERVER, OPTIONS);
        // in loopback's case the SERVER is app.start();
        const io = require("socket.io")(app.start(), options);

        // now here we can do the usual io.on('connection' socket => { ... });

        // setting this means that you can use the io instance anywhere you use app;
        app.io = io;
    }
});

Client Side

The client side socket instance is based on the Context API from React

providing the socket:

wrap your component tree with the SocketProvider component:

import { SocketProvider } from "${path}/modules/socket.io";

<SocketProvider
    uri="localhost:8080"
    options={{ query: `token=${accessToken}` }}
>
    <App />
</SocketProvider>;

connecting to the socket depends on where you use SocketProvider as it connects before the initial render of the the component;

SocketProvider Props:

uri (required): The uri that we'll connect to, including the namespace, where '/' is the default one (e.g. http://localhost:4000/secret-admin);

options (optional): Any connect options that we want to pass along: https://socket.io/docs/client-api/#io-url-options

Consuming the Socket instance:

The way the components get access to the socket instance is through the context object SocketContext, that you can import, but these next two ways are simpler:

  1. In a class based components, You can wrap your component with withSocket and recieve the socket via the socket prop:
import { withSocket } from "./${PATH}/modules/socket.io";

class MyComponent extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            messages: [],
        };
    }

    componentDidMount() {
        this.props.socket.emit("join", this.props.chatId);

        this.props.socket.on("message-recieved", (message) => {
            this.setState(({ messages }) => ({
                messages: [...messages, message],
            }));
        });
    }

    render() {
        return (
            <div>
                {this.state.messages.map((message) => (
                    <div>{message}</div>
                ))}
            </div>
        );
    }
}

export default withSocket(MyComponent);
  1. In a functional component, you can use the hook "useSocket":
import { useSocket } from "./${PATH}/modules/socket.io";

const MyComponent = ({ chatId }) => {
    const [messages, setMessages] = usestate([]);
    const socket = useSocket();

    useEffect(() => {
        socket.emit("join", chatId);

        socket.on("message-recieved", (message) => {
            setMessages((messages) => [...messages, message]);
        });
    }, []);

    return (
        <div>
            {messages.map((message) => (
                <div>{message}</div>
            ))}
        </div>
    );
};

export default MyComponent;

That's the end of how you implenent the socket in React and Loopback;

Extra Exports

Generic events:

the file genericEvents.js includes some generic events like 'connection' 'disconnected' and more. Other custom events are also included like 'JOIN' 'LEAVE' events

Now let's look at the extra exports in the client and server:

Client:

join, leave and getRooms:

We added four methods to the socket:

/**
 * Emits the 'JOIN' event
 * @param name The name of the room that we want to join
 * @param fn An optional callback to call when we've joined the room. It should take an optional parameter, err, of a possible error
 */
join(name: string | string[], fn?: (err?: any) => void): Promise<void>;

/**
 * Emits the 'LEAVE' event
 * @param name The name of the room to leave
 * @param fn An optional callback to call when we've left the room. It should take on optional parameter, err, of a possible error
 */
leave(name: string, fn?: Function): Promise<void>;

/**
 * Emits the 'GET_ROOMS' event
 */
getRooms(fn?: (rooms: SocketRooms) => void): Promise<SocketRooms>;

/**
 * Emits the 'LEAVE_ALL' event
 */
leaveAll(): void;

Note that the socket will not join or leave or get the rooms. This is just in the client. If you want the functionality you need to listen to the events in the server or import the generic-io-server.js file and pass the io to it.

If you include the generic-io-server.js file you will get this functionality:

  • join & leave will return a promise so you can await it, if there is an error, the promise will get rejected. if you prefer, you can pass a callback that recieves the err.
  • getRooms will return a promise so you can await it. The resolved value will be the rooms the socket is in, in the server. if you prefer, you can pass a callback that recieves the rooms.
  • leaveAll will leave all the rooms the socket is in.

extra hooks:

useOn

hook that listens to event;

parameters:

  • event (required): the event to listen to;
  • listener (required): the listener function;
  • dependency list (optional): the hook returns the listener wrapped by the useCallback hook. the dependency list is passed to useCallback. if it's not provided it will just return the listener;

example:

const MyComponent = () => {
    const [count, setCount] = useState(0);

    // called on the 'hello' event;
    useOn("hello", () => {
        console.log("someone emited hello");
    });

    // increment gets called on the 'increment' event and when the div is clicked;
    const increment = useOn(
        "increment",
        () => {
            setCount(count + 1);
        },
        [count]
    );

    return <div onClick={increment}>component using useOn</div>;
};

export default MyComponent;

useStateOn

hook that combines useState and useOn: it listens to event and sets the state on the event;

parameters:

  • event (required): the event to listen to;
  • initial state (optional): the initial state;

example:

const MyComponent = () => {
    // returns regular state tuple and on the 'count' event it will set the state to the recieved argument;
    const [count, setCount] = useStateOn("count", 0);

    return (
        <div onClick={() => setCount((count) => count + 1)}>
            component using useStateOn
        </div>
    );
};

export default MyComponent;

useStateEmit

hook that returns a useState tuple and a function that sets the new state and emits it;

parameters:

  • event (required): the event to listen to;
  • initial state (required): the initial state;

example:

const MyComponent = ({ id }) => {
    // returns regular useState tuple with the emitter;
    const [count, setCount, setCountEmit] = useStateEmit("count", 0);

    useEffect(() => {
        setInterval(() => {
            // each time setCount is called it emits the 'count' event with the next state;
            // note: you can send extra arguments to the server;
            setCountEmit((count) => count + 1, id);
        }, 1000);
    }, []);

    return <div>component using useStateEmit</div>;
};

export default MyComponent;

useStateSocket

hook that combines useStateOn and useStateEmit;

parameters:

  • event (required): the event to listen to;
  • initial state (required): the initial state;

example:

const MyComponent = ({ id }) => {
    // returns regular state tuple with the emitter and on the 'count' event it will set the state to the recieved argument;
    const [count, setCount, setCountEmit] = useStateSocket("count", 0);

    useEffect(() => {
        setInterval(() => {
            // each time setCount is called it emits the 'count' event with the next state;
            // note: you can send extra arguments to the server;
            setCountEmit((count) => count + 1, id);
        }, 1000);
    }, []);

    return <div>component using useStateSocket</div>;
};

export default MyComponent;

Server:

Authentication:

const cookieParser = require("cookie-parser");
const cookie = require("cookie");

After setting the io instance:

io.use((socket, next) => {
    // you can access the cookies through socket.request.headers.cookie;
    // call the next() function if the the client is authenticated;
    // here's an example for authenticating in loopback;
    (async () => {
        try {
            const accessToken = cookie.parse(socket.request.headers.cookie)[
                "access_token"
            ];
            if (!accessToken) throw { message: "no access token" };
            const { AccessToken } = app.models;
            const token = cookieParser.signedCookie(
                decodeURIComponent(accessToken),
                app.get("cookieSecret")
            );
            const res = await AccessToken.findById(token);
            if (!res) throw { message: "incorrect access token" };
            next();
        } catch (err) {
            console.log(err);
        }
    })();
});

Listening to changes to models:

**if you have data that changes every few seconds/milliseconds rather don't use this because the data will only be emitted when it's added to the server which can take a while;

const afterHookEmit = require(afterHookEmit_FILE_PATH);

After setting app.io = io;

afterHookEmit(app, MODELS);

MODELS Example:

const MODELS = [
    {
        model: "AssistantRides", // The model name;
        room: "Rides", // the name of the room to send the data to. Default to the model name;
        roomId: "rideId", // The name of the room Id. default to "id";
        include: ["assistants", "rides"], // Not required, can pass relations to include;
        disableAfterDelete: true, // default false; means that when an instance of a model is deleted it won't emit (kind of buggy so make true for the meantime);
    },
];

Say we have the models AssistantRides and Rides. we want to listen for changes in AssistantRides and emit to the room name Rides. if the AssistantRides instance is like this

{ id: 234, rideId: 567, moreInfo: {} }

the emited event will be: 'Rides' and will be sent to the 'Rides-567' room, and will include the info that will look like this

const data = {
    model: "Rides",
    method: "CREATE", // options: "CREATE" | "UPDATE" | "DELETE"
    instance: { id: 234, rideId: 567, moreInfo: {} },
    include: {
        assistants: [{ info: "" }],
        rides: { info: "" },
    },
};

Listening In the client:

import { withSocket } from "./${PATH}/modules/socket.io";

class MyComponent extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            assistants: [],
        };
    }

    async componentDidMount() {
        this.props.socket.on("Rides", (data) => {
            const { assistants } = data.include;
            this.setState({ assistants });
        });

        let [rideId, error] = await Auth.superAuthFetch(`/api/Rides/getRideId`);
        if (response.error || error) {
            console.error("ERR: ", error || response.error);
            return;
        }

        // in the server you need to listen to the 'join' event and join the room;
        this.props.socket.emit("join", `Rides-${rideId}`);
    }

    render() {
        return (
            <div>
                {this.state.assistants.map((assistant) => (
                    <div>{assistant}</div>
                ))}
            </div>
        );
    }
}

export default withSocket(MyComponent);
10.0.0

4 years ago

7.0.4

4 years ago

7.0.3

4 years ago

7.0.2

4 years ago

4.0.0

4 years ago

5.0.0

4 years ago

7.0.0

4 years ago

6.0.0

4 years ago

3.0.0

4 years ago

7.0.1

4 years ago

1.0.2

4 years ago

1.0.1

4 years ago

1.0.0

4 years ago