A dynamic block renders in PHP and stores nothing

A block showing the five most recent posts cannot store its output, because the output is different tomorrow. A render callback runs on every page load instead.

register_block_type( 'turkerdev/recent', array(
    'attributes'      => array(
        'count' => array( 'type' => 'number', 'default' => 5 ),
    ),
    'render_callback' => function ( $attributes ) {
        $posts = get_posts( array( 'numberposts' => $attributes['count'] ) );

        $out = '<ul class="wp-block-turkerdev-recent">';
        foreach ( $posts as $post ) {
            $out .= '<li>' . esc_html( get_the_title( $post ) ) . '</li>';
        }

        return $out . '</ul>';
    },
) );

The save function in JavaScript returns null for this kind of block, which is what tells the editor not to expect markup between the delimiters. That also means dynamic blocks never produce a validation error, which makes them tempting for cases that do not need them — the trade is a database query on every page view where a static block would have cost nothing. Escaping is entirely yours here, exactly as it always was in a shortcode.